diff --git a/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs index 786df2785..c49796654 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs @@ -9,7 +9,6 @@ using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Jupyter.Protocol; -using WorkspaceServer.Kernel; namespace Microsoft.DotNet.Interactive.Jupyter { @@ -31,7 +30,7 @@ public async Task Handle(JupyterRequestContext context) var command = new RequestCompletion(completeRequest.Code, completeRequest.CursorPosition); - var openRequest = new InflightRequest(context, completeRequest, 0, null); + var openRequest = new InflightRequest(context, completeRequest, 0); InFlightRequests[command] = openRequest; diff --git a/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs index 85f0c1ddb..9440d8215 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs @@ -10,14 +10,13 @@ using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Jupyter.Protocol; -using WorkspaceServer.Kernel; namespace Microsoft.DotNet.Interactive.Jupyter { public class ExecuteRequestHandler : RequestHandlerBase { private int _executionCount; - + public ExecuteRequestHandler(IKernel kernel) : base(kernel) { } @@ -30,9 +29,8 @@ public async Task Handle(JupyterRequestContext context) var executionCount = executeRequest.Silent ? _executionCount : Interlocked.Increment(ref _executionCount); var command = new SubmitCode(executeRequest.Code, "csharp"); - var id = Guid.NewGuid(); - var transient = new Dictionary { { "display_id", id.ToString() } }; - var openRequest = new InflightRequest(context, executeRequest, executionCount, transient); + + var openRequest = new InflightRequest(context, executeRequest, executionCount); InFlightRequests[command] = openRequest; @@ -79,6 +77,13 @@ public async Task Handle(JupyterRequestContext context) } } + private static Dictionary CreateTransient() + { + var id = Guid.NewGuid(); + var transient = new Dictionary { { "display_id", id.ToString() } }; + return transient; + } + void OnKernelResultEvent(IKernelEvent value) { switch (value) @@ -96,7 +101,7 @@ void OnKernelResultEvent(IKernelEvent value) case IncompleteCodeSubmissionReceived _: case CompleteCodeSubmissionReceived _: break; - default: + default: throw new NotSupportedException(); } } @@ -152,12 +157,18 @@ private static void OnValueProduced( try { + var transient = CreateTransient(); // executeResult data - var executeResultData = new ExecuteResult( + var executeResultData = valueProduced.IsLastValue + ? new ExecuteResult( openRequest.ExecutionCount, - transient: openRequest.Transient, + transient: transient, + data: valueProduced?.FormattedValues + ?.ToDictionary(k => k.MimeType, v => v.Value)) + : new DisplayData( + transient: transient, data: valueProduced?.FormattedValues - ?.ToDictionary(k => k.MimeType ?? "text/plain", v => v.Value)); + ?.ToDictionary(k => k.MimeType, v => v.Value)); if (!openRequest.Request.Silent) { diff --git a/Microsoft.DotNet.Interactive.Jupyter/RequestHandlerBase.cs b/Microsoft.DotNet.Interactive.Jupyter/RequestHandlerBase.cs index a627868d7..bd43725e5 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/RequestHandlerBase.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/RequestHandlerBase.cs @@ -7,7 +7,6 @@ using System.Reactive.Disposables; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Jupyter.Protocol; -using WorkspaceServer.Kernel; namespace Microsoft.DotNet.Interactive.Jupyter { @@ -41,18 +40,15 @@ public void Dispose() protected class InflightRequest : IDisposable { private readonly CompositeDisposable _disposables = new CompositeDisposable(); - public Dictionary Transient { get; } public JupyterRequestContext Context { get; } public T Request { get; } public int ExecutionCount { get; } - public InflightRequest(JupyterRequestContext context, T request, int executionCount, - Dictionary transient) + public InflightRequest(JupyterRequestContext context, T request, int executionCount) { Context = context; Request = request; ExecutionCount = executionCount; - Transient = transient; } public void AddDisposable(IDisposable disposable) diff --git a/Microsoft.DotNet.Interactive.Rendering/Formatter.cs b/Microsoft.DotNet.Interactive.Rendering/Formatter.cs index 017b05a86..3ec984932 100644 --- a/Microsoft.DotNet.Interactive.Rendering/Formatter.cs +++ b/Microsoft.DotNet.Interactive.Rendering/Formatter.cs @@ -18,6 +18,7 @@ namespace Microsoft.DotNet.Interactive.Rendering /// public static class Formatter { + private const string DefaultMimeType = "text/plain"; private static Func _autoGenerateForType = t => false; private static int _defaultListExpansionLimit; private static int _recursionLimit; @@ -358,8 +359,7 @@ private static void TryRegisterDefault(string typeName, Action formattedValues = null) : base(submitCode) { Value = value; + IsLastValue = isLastValue; FormattedValues = formattedValues; } public object Value { get; } + public bool IsLastValue { get; } public IReadOnlyCollection FormattedValues { get; } } diff --git a/Microsoft.DotNet.Interactive/FormattedValue.cs b/Microsoft.DotNet.Interactive/FormattedValue.cs index e71da82ca..3eae78368 100644 --- a/Microsoft.DotNet.Interactive/FormattedValue.cs +++ b/Microsoft.DotNet.Interactive/FormattedValue.cs @@ -1,12 +1,19 @@ // 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 Microsoft.DotNet.Interactive { public class FormattedValue { public FormattedValue(string mimeType, object value) { + if (string.IsNullOrWhiteSpace(mimeType)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(mimeType)); + } + MimeType = mimeType; Value = value; } diff --git a/WorkspaceServer.Tests/Kernel/CSharpKernelTests.cs b/WorkspaceServer.Tests/Kernel/CSharpKernelTests.cs index 7a21a5bbf..bd96b79c9 100644 --- a/WorkspaceServer.Tests/Kernel/CSharpKernelTests.cs +++ b/WorkspaceServer.Tests/Kernel/CSharpKernelTests.cs @@ -30,7 +30,7 @@ public async Task it_returns_the_result_of_a_non_null_expression() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("123", "csharp")); + await kernel.SendAsync(new SubmitCode("123")); KernelEvents.OfType() .Last() @@ -44,9 +44,9 @@ public async Task when_it_throws_exception_after_a_value_was_produced_then_only_ { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("using System;", "csharp")); - await kernel.SendAsync(new SubmitCode("2 + 2", "csharp")); - await kernel.SendAsync(new SubmitCode("adddddddddd", "csharp")); + await kernel.SendAsync(new SubmitCode("using System;")); + await kernel.SendAsync(new SubmitCode("2 + 2")); + await kernel.SendAsync(new SubmitCode("adddddddddd")); var (failure, lastCodeSubmissionEvaluationFailedPosition) = KernelEvents .Select((error, pos) => (error, pos)) @@ -75,8 +75,8 @@ public async Task it_returns_exceptions_thrown_in_user_code() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("using System;", "csharp")); - await kernel.SendAsync(new SubmitCode("throw new NotImplementedException();", "csharp")); + await kernel.SendAsync(new SubmitCode("using System;")); + await kernel.SendAsync(new SubmitCode("throw new NotImplementedException();")); KernelEvents.Last() .Should() @@ -92,8 +92,8 @@ public async Task it_returns_diagnostics() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("using System;", "csharp")); - await kernel.SendAsync(new SubmitCode("aaaadd", "csharp")); + await kernel.SendAsync(new SubmitCode("using System;")); + await kernel.SendAsync(new SubmitCode("aaaadd")); KernelEvents.Last() .Should() @@ -109,9 +109,9 @@ public async Task it_notifies_when_submission_is_complete() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("var a =", "csharp")); + await kernel.SendAsync(new SubmitCode("var a =")); - await kernel.SendAsync(new SubmitCode("12;", "csharp")); + await kernel.SendAsync(new SubmitCode("12;")); KernelEvents.Should() .NotContain(e => e is ValueProduced); @@ -126,7 +126,7 @@ public async Task it_notifies_when_submission_is_incomplete() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("var a =", "csharp")); + await kernel.SendAsync(new SubmitCode("var a =")); KernelEvents.Should() .NotContain(e => e is ValueProduced); @@ -141,7 +141,7 @@ public async Task it_returns_the_result_of_a_null_expression() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("null", "csharp")); + await kernel.SendAsync(new SubmitCode("null")); KernelEvents.OfType() .Last() @@ -155,7 +155,7 @@ public async Task it_does_not_return_a_result_for_a_statement() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("var x = 1;", "csharp")); + await kernel.SendAsync(new SubmitCode("var x = 1;")); KernelEvents .Should() @@ -167,9 +167,9 @@ public async Task it_aggregates_multiple_submissions() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("var x = new List{1,2};", "csharp")); - await kernel.SendAsync(new SubmitCode("x.Add(3);", "csharp")); - await kernel.SendAsync(new SubmitCode("x.Max()", "csharp")); + await kernel.SendAsync(new SubmitCode("var x = new List{1,2};")); + await kernel.SendAsync(new SubmitCode("x.Add(3);")); + await kernel.SendAsync(new SubmitCode("x.Max()")); KernelEvents.OfType() .Last() @@ -178,13 +178,52 @@ public async Task it_aggregates_multiple_submissions() .Be(3); } + [Fact] + public async Task it_produces_values_when_executing_Console_output() + { + var kernel = CreateKernel(); + + var kernelCommand = new SubmitCode(@" +Console.Write(""value one""); +Console.Write(""value two""); +Console.Write(""value three"");"); + await kernel.SendAsync(kernelCommand); + + KernelEvents.OfType() + .Should() + .BeEquivalentTo( + new ValueProduced("value one", kernelCommand, false, new[] { new FormattedValue("text/plain", "value one"), }), + new ValueProduced("value two", kernelCommand, false, new[] { new FormattedValue("text/plain", "value two"), }), + new ValueProduced("value three", kernelCommand, false, new[] { new FormattedValue("text/plain", "value three"), })); + } + + [Fact] + public async Task it_produces_a_final_value_if_the_code_expression_evaluates() + { + var kernel = CreateKernel(); + + var kernelCommand = new SubmitCode(@" +Console.Write(""value one""); +Console.Write(""value two""); +Console.Write(""value three""); +5", "csharp"); + await kernel.SendAsync(kernelCommand); + + KernelEvents.OfType() + .Should() + .HaveCount(4) + .And + .ContainSingle(e => e.IsLastValue); + + } + [Fact(Skip = "requires support for cs8 in roslyn scripting")] public async Task it_supports_csharp_8() { var kernel = CreateKernel(); - await kernel.SendAsync(new SubmitCode("var text = \"meow? meow!\";", "csharp")); - await kernel.SendAsync(new SubmitCode("text[^5..^0]", "csharp")); + await kernel.SendAsync(new SubmitCode("var text = \"meow? meow!\";")); + await kernel.SendAsync(new SubmitCode("text[^5..^0]")); KernelEvents.OfType() .Last() @@ -280,7 +319,7 @@ await kernel.SendAsync( .Should() .Contain(i => i.DisplayText == "SerializeObject"); } - + [Fact] public async Task The_extend_directive_can_be_used_to_load_a_kernel_extension() { diff --git a/WorkspaceServer/Kernel/CSharpKernel.cs b/WorkspaceServer/Kernel/CSharpKernel.cs index b7c9702ec..d6b049e8c 100644 --- a/WorkspaceServer/Kernel/CSharpKernel.cs +++ b/WorkspaceServer/Kernel/CSharpKernel.cs @@ -20,6 +20,7 @@ using Microsoft.DotNet.Interactive.Events; using WorkspaceServer.LanguageServices; using Microsoft.DotNet.Interactive.Rendering; +using WorkspaceServer.Servers.Roslyn; using WorkspaceServer.Servers.Scripting; using CompletionItem = Microsoft.DotNet.Interactive.CompletionItem; using Task = System.Threading.Tasks.Task; @@ -106,7 +107,7 @@ protected override async Task HandleAsync( } private async Task HandleSubmitCode( - SubmitCode codeSubmission, + SubmitCode codeSubmission, KernelInvocationContext context) { var codeSubmissionReceived = new CodeSubmissionReceived( @@ -120,29 +121,38 @@ private async Task HandleSubmitCode( { context.OnNext(new CompleteCodeSubmissionReceived(codeSubmission)); Exception exception = null; - try + var output = Array.Empty(); + using (var console = await ConsoleOutput.Capture()) { - if (_scriptState == null) + + try { - _scriptState = await CSharpScript.RunAsync( - code, - ScriptOptions); + if (_scriptState == null) + { + _scriptState = await CSharpScript.RunAsync( + code, + ScriptOptions); + } + else + { + _scriptState = await _scriptState.ContinueWithAsync( + code, + ScriptOptions, + e => + { + exception = e; + return true; + }); + } } - else + catch (Exception e) { - _scriptState = await _scriptState.ContinueWithAsync( - code, - ScriptOptions, - e => - { - exception = e; - return true; - }); + exception = e; } - } - catch (Exception e) - { - exception = e; + output = + console.WriteOccurredOnStandardOutput + ? console.GetStandardOutputWrites().ToArray() + : Array.Empty(); } if (exception != null) @@ -155,6 +165,21 @@ private async Task HandleSubmitCode( } else { + foreach (var std in output) + { + var formattedValues = new List + { + new FormattedValue( + Formatter.MimeTypeFor(std?.GetType() ?? typeof(object)), std) + }; + + context.OnNext( + new ValueProduced( + std, + codeSubmission, + false, + formattedValues)); + } if (HasReturnValue) { var writer = new StringWriter(); @@ -170,6 +195,7 @@ private async Task HandleSubmitCode( new ValueProduced( _scriptState.ReturnValue, codeSubmission, + true, formattedValues)); } @@ -186,11 +212,11 @@ private async Task HandleSubmitCode( private async Task HandleRequestCompletion( RequestCompletion requestCompletion, - KernelInvocationContext context, + KernelInvocationContext context, ScriptState scriptState) { var completionRequestReceived = new CompletionRequestReceived(requestCompletion); - + context.OnNext(completionRequestReceived); var completionList = @@ -208,7 +234,7 @@ public void AddMetatadaReferences(IEnumerable references) private async Task> GetCompletionList(string code, int cursorPosition, ScriptState scriptState) { var metadataReferences = ImmutableArray.Empty; - + var forcedState = false; if (scriptState == null) { @@ -223,14 +249,14 @@ private async Task> GetCompletionList(string code, i buffer.AppendLine(code); var fullScriptCode = buffer.ToString(); var offset = fullScriptCode.LastIndexOf(code, StringComparison.InvariantCulture); - var absolutePosition = Math.Max(offset,0) + cursorPosition; + var absolutePosition = Math.Max(offset, 0) + cursorPosition; if (_fixture == null || _metadataReferences != metadataReferences) { _fixture = new WorkspaceFixture(compilation.Options, metadataReferences); _metadataReferences = metadataReferences; } - + var document = _fixture.ForkDocument(fullScriptCode); var service = CompletionService.GetService(document); @@ -253,7 +279,7 @@ private async Task> GetCompletionList(string code, i } private bool HasReturnValue => - _scriptState != null && - (bool) _hasReturnValueMethod.Invoke(_scriptState.Script, null); + _scriptState != null && + (bool)_hasReturnValueMethod.Invoke(_scriptState.Script, null); } } \ No newline at end of file diff --git a/WorkspaceServer/Servers/Roslyn/ConsoleOutput.cs b/WorkspaceServer/Servers/Roslyn/ConsoleOutput.cs index 5c46f805d..eedde401b 100644 --- a/WorkspaceServer/Servers/Roslyn/ConsoleOutput.cs +++ b/WorkspaceServer/Servers/Roslyn/ConsoleOutput.cs @@ -2,19 +2,20 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; -using Clockwise; namespace WorkspaceServer.Servers.Roslyn { public class ConsoleOutput : IDisposable { + private TextWriter originalOutputWriter; private TextWriter originalErrorWriter; - private readonly StringWriter outputWriter = new StringWriter(); - private readonly StringWriter errorWriter = new StringWriter(); + private readonly TrackingStringWriter outputWriter = new TrackingStringWriter(); + private readonly TrackingStringWriter errorWriter = new TrackingStringWriter(); private const int NOT_DISPOSED = 0; private const int DISPOSED = 1; @@ -66,9 +67,15 @@ public void Dispose() } } + + public string StandardOutput => outputWriter.ToString(); public string StandardError => errorWriter.ToString(); + public bool WriteOccurredOnStandardOutput => outputWriter.WriteOccurred; + + public bool WriteOccurredOnStandardError => errorWriter.WriteOccurred; + public void Clear() { @@ -77,5 +84,10 @@ public void Clear() } public bool IsEmpty() => outputWriter.ToString().Length == 0 && errorWriter.ToString().Length == 0; + + public IEnumerable GetStandardOutputWrites() + { + return outputWriter.Writes(); + } } } diff --git a/WorkspaceServer/Servers/Roslyn/TrackingStringWriter.cs b/WorkspaceServer/Servers/Roslyn/TrackingStringWriter.cs new file mode 100644 index 000000000..84ac20015 --- /dev/null +++ b/WorkspaceServer/Servers/Roslyn/TrackingStringWriter.cs @@ -0,0 +1,294 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace WorkspaceServer.Servers.Roslyn +{ + internal class TrackingStringWriter : StringWriter + { + private class Region + { + public int Start { get; set; } + public int Length { get; set; } + } + + readonly List _regions = new List(); + + private bool _trackingWriteOperation; + + public bool WriteOccurred { get; set; } + + public override void Write(char value) + { + TrackWriteOperation(() => base.Write(value)); + } + + private void TrackWriteOperation(Action action) + { + WriteOccurred = true; + if (_trackingWriteOperation) + { + action(); + return; + } + + _trackingWriteOperation = true; + var sb = base.GetStringBuilder(); + + var region = new Region + { + Start = sb.Length + }; + + _regions.Add(region); + + action(); + + region.Length = sb.Length - region.Start; + _trackingWriteOperation = false; + } + + private async Task TrackWriteOperationAsync(Func action) + { + WriteOccurred = true; + if (_trackingWriteOperation) + { + await action(); + return; + } + _trackingWriteOperation = true; + var sb = base.GetStringBuilder(); + + var region = new Region + { + Start = sb.Length + }; + + _regions.Add(region); + + await action(); + + region.Length = sb.Length - region.Start; + _trackingWriteOperation = false; + } + + public override void Write(char[] buffer, int index, int count) + { + TrackWriteOperation(() => base.Write(buffer, index, count)); + } + + public override void Write(string value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override Task WriteAsync(char value) + { + return TrackWriteOperationAsync(() => base.WriteAsync(value)); + } + + public override Task WriteAsync(char[] buffer, int index, int count) + { + return TrackWriteOperationAsync(() => base.WriteAsync(buffer, index, count)); + } + + public override Task WriteAsync(string value) + { + return TrackWriteOperationAsync(() => base.WriteAsync(value)); + } + + public override Task WriteLineAsync(char value) + { + return TrackWriteOperationAsync(() => base.WriteLineAsync(value)); + } + + public override Task WriteLineAsync(char[] buffer, int index, int count) + { + return TrackWriteOperationAsync(() => base.WriteLineAsync(buffer, index, count)); + } + + public override Task WriteLineAsync(string value) + { + return TrackWriteOperationAsync(() => base.WriteLineAsync(value)); + } + + public override void Write(bool value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(char[] buffer) + { + TrackWriteOperation(() => base.Write(buffer)); + } + + public override void Write(decimal value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(double value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(int value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(long value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(object value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(float value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(string format, object arg0) + { + TrackWriteOperation(() => base.Write(format, arg0)); + } + + public override void Write(string format, object arg0, object arg1) + { + TrackWriteOperation(() => base.Write(format, arg0, arg1)); + } + + public override void Write(string format, object arg0, object arg1, object arg2) + { + TrackWriteOperation(() => base.Write(format, arg0, arg1, arg2)); + } + + public override void Write(string format, params object[] arg) + { + TrackWriteOperation(() => base.Write(format, arg)); + } + + public override void Write(uint value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void Write(ulong value) + { + TrackWriteOperation(() => base.Write(value)); + } + + public override void WriteLine() + { + TrackWriteOperation(() => base.WriteLine()); + } + + public override void WriteLine(bool value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(char value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(char[] buffer) + { + TrackWriteOperation(() => base.WriteLine(buffer)); + } + + public override void WriteLine(char[] buffer, int index, int count) + { + TrackWriteOperation(() => base.WriteLine(buffer, index, count)); + } + + public override void WriteLine(decimal value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(double value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(int value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(long value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(object value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(float value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(string value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(string format, object arg0) + { + TrackWriteOperation(() => base.WriteLine(format, arg0)); + } + + public override void WriteLine(string format, object arg0, object arg1) + { + TrackWriteOperation(() => base.WriteLine(format, arg0, arg1)); + } + + public override void WriteLine(string format, object arg0, object arg1, object arg2) + { + TrackWriteOperation(() => base.WriteLine(format, arg0, arg1, arg2)); + } + + public override void WriteLine(string format, params object[] arg) + { + TrackWriteOperation(() => base.WriteLine(format, arg)); + } + + public override void WriteLine(uint value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override void WriteLine(ulong value) + { + TrackWriteOperation(() => base.WriteLine(value)); + } + + public override Task WriteLineAsync() + { + return TrackWriteOperationAsync(() => base.WriteLineAsync()); + } + + public IEnumerable Writes() + { + var src = base.GetStringBuilder().ToString(); + foreach (var region in _regions) + { + yield return src.Substring(region.Start, region.Length); + } + } + } +} \ No newline at end of file