Skip to content

Commit c2e7979

Browse files
Support for Span<T>/ReadOnlySpan<T> outputs + formatting. Resolves #318.
1 parent 490f95c commit c2e7979

File tree

7 files changed

+294
-88
lines changed

7 files changed

+294
-88
lines changed

CSharpRepl.Services/CSharpRepl.Services.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
</AssemblyAttribute>
5555
</ItemGroup>
5656

57+
58+
<ItemGroup>
59+
<EmbeddedResource Include="RuntimeHelper.cs" />
60+
</ItemGroup>
61+
5762
<ItemGroup>
5863
<None Update="runtime.json">
5964
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/IEnumerableFormatter.cs

Lines changed: 115 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -22,45 +22,46 @@ public override StyledString FormatToText(IEnumerable value, Level level, Format
2222
var sb = new StyledStringBuilder();
2323

2424
//header
25-
AppendHeader(sb, value, formatter);
26-
27-
//items
28-
sb.Append(" { ");
29-
var enumerator = value.GetEnumerator();
30-
try
25+
if (AppendHeader(sb, value, formatter))
3126
{
32-
var maxParagraphLength = LengthLimiting.GetMaxParagraphLength(level, formatter.ConsoleProfile);
33-
bool any = false;
34-
while (enumerator.MoveNext())
27+
//items
28+
sb.Append(" { ");
29+
var enumerator = value.GetEnumerator();
30+
try
3531
{
36-
if (any)
32+
var maxParagraphLength = LengthLimiting.GetMaxParagraphLength(level, formatter.ConsoleProfile);
33+
bool any = false;
34+
while (enumerator.MoveNext())
3735
{
38-
sb.Append(", ");
39-
40-
if (maxParagraphLength > sb.Length &&
41-
sb.Length > formatter.ConsoleProfile.Width / 2) //just heuristic
36+
if (any)
4237
{
43-
sb.Append(", ...");
44-
break;
38+
sb.Append(", ");
39+
40+
if (maxParagraphLength > sb.Length &&
41+
sb.Length > formatter.ConsoleProfile.Width / 2) //just heuristic
42+
{
43+
sb.Append(", ...");
44+
break;
45+
}
4546
}
47+
var formattedItem = formatter.FormatObjectToText(enumerator.Current, level.Increment());
48+
sb.Append(formattedItem);
49+
any = true;
4650
}
47-
var formattedItem = formatter.FormatObjectToText(enumerator.Current, level.Increment());
48-
sb.Append(formattedItem);
49-
any = true;
5051
}
51-
}
52-
catch (Exception ex)
53-
{
54-
sb.Append(formatter.GetValueRetrievalExceptionText(ex, level.Increment()));
55-
}
56-
finally
57-
{
58-
if (enumerator is IDisposable disposable)
52+
catch (Exception ex)
53+
{
54+
sb.Append(formatter.GetValueRetrievalExceptionText(ex, level.Increment()));
55+
}
56+
finally
5957
{
60-
disposable.Dispose();
58+
if (enumerator is IDisposable disposable)
59+
{
60+
disposable.Dispose();
61+
}
6162
}
63+
sb.Append(" }");
6264
}
63-
sb.Append(" }");
6465

6566
return sb.ToStyledString();
6667
}
@@ -73,79 +74,113 @@ public override FormattedObjectRenderable FormatToRenderable(IEnumerable value,
7374
}
7475

7576
var sb = new StyledStringBuilder();
76-
77-
AppendHeader(sb, value, formatter);
78-
var header = sb.ToStyledString().ToParagraph();
79-
80-
var table = new Table().AddColumns("Name", "Value", "Type");
81-
82-
var enumerator = value.GetEnumerator();
83-
try
77+
if (AppendHeader(sb, value, formatter))
8478
{
85-
var maxItems = LengthLimiting.GetTableMaxItems(level, formatter.ConsoleProfile);
86-
int counter = 0;
87-
while (enumerator.MoveNext())
79+
var header = sb.ToStyledString().ToParagraph();
80+
var table = new Table().AddColumns("Name", "Value", "Type");
81+
82+
var enumerator = value.GetEnumerator();
83+
try
8884
{
89-
if (counter > maxItems)
85+
var maxItems = LengthLimiting.GetTableMaxItems(level, formatter.ConsoleProfile);
86+
int counter = 0;
87+
while (enumerator.MoveNext())
9088
{
91-
table.AddRow("...", "...", "...");
92-
break;
93-
}
89+
if (counter > maxItems)
90+
{
91+
table.AddRow("...", "...", "...");
92+
break;
93+
}
9494

95-
sb.Clear();
96-
sb.Append('[').Append(formatter.FormatObjectToText(counter, Level.FirstSimple)).Append(']');
95+
sb.Clear();
96+
sb.Append('[').Append(formatter.FormatObjectToText(counter, Level.FirstSimple)).Append(']');
9797

98-
var name = sb.ToStyledString();
98+
var name = sb.ToStyledString();
9999

100-
var itemValue = formatter.FormatObjectToRenderable(enumerator.Current, level.Increment());
100+
var itemValue = formatter.FormatObjectToRenderable(enumerator.Current, level.Increment());
101101

102-
var itemType =
103-
enumerator.Current is null ?
104-
new Paragraph("") :
105-
formatter.FormatObjectToText(enumerator.Current.GetType(), level.Increment()).ToParagraph();
102+
var itemType =
103+
enumerator.Current is null ?
104+
new Paragraph("") :
105+
formatter.FormatObjectToText(enumerator.Current.GetType(), level.Increment()).ToParagraph();
106106

107-
table.AddRow(name.ToParagraph(), itemValue, itemType);
107+
table.AddRow(name.ToParagraph(), itemValue, itemType);
108108

109-
counter++;
110-
}
109+
counter++;
110+
}
111111

112-
if (counter == 0)
112+
if (counter == 0)
113+
{
114+
return new FormattedObjectRenderable(header, renderOnNewLine: false);
115+
}
116+
}
117+
catch (Exception ex)
118+
{
119+
table.AddRow(new Paragraph(""), formatter.GetValueRetrievalExceptionText(ex, level.Increment()).ToParagraph(), new Paragraph(""));
120+
}
121+
finally
113122
{
114-
return new FormattedObjectRenderable(header, renderOnNewLine: false);
123+
if (enumerator is IDisposable disposable)
124+
{
125+
disposable.Dispose();
126+
}
115127
}
128+
129+
return new FormattedObjectRenderable(
130+
new RenderableSequence(header, table, separateByLineBreak: true),
131+
renderOnNewLine: false);
132+
}
133+
else
134+
{
135+
return new FormattedObjectRenderable(sb.ToStyledString().ToParagraph(), renderOnNewLine: false);
116136
}
117-
catch (Exception ex)
137+
}
138+
139+
private static bool AppendHeader(StyledStringBuilder sb, IEnumerable value, Formatter formatter)
140+
{
141+
var type = value.GetType();
142+
143+
if (type.FullName?.EndsWith(typeof(__CSharpRepl_RuntimeHelper.CharSpanOutput).FullName!) == true)
118144
{
119-
table.AddRow(new Paragraph(""), formatter.GetValueRetrievalExceptionText(ex, level.Increment()).ToParagraph(), new Paragraph(""));
145+
if (type.GetField(nameof(__CSharpRepl_RuntimeHelper.CharSpanOutput.Text))?.GetValue(value) is string text &&
146+
type.GetField(nameof(__CSharpRepl_RuntimeHelper.CharSpanOutput.SpanWasReadOnly))?.GetValue(value) is bool readOnly)
147+
{
148+
type = readOnly ? typeof(ReadOnlySpan<char>) : typeof(Span<char>);
149+
150+
AppendTypeName(isArray: false);
151+
sb.Append(Environment.NewLine);
152+
sb.Append(formatter.FormatObjectToText(text, Level.FirstDetailed));
153+
return false;
154+
}
120155
}
121-
finally
156+
else if (type.FullName?.EndsWith(typeof(__CSharpRepl_RuntimeHelper.SpanOutput).FullName!) == true)
122157
{
123-
if (enumerator is IDisposable disposable)
158+
if (type.GetField(nameof(__CSharpRepl_RuntimeHelper.SpanOutput.Array))?.GetValue(value) is Array array &&
159+
type.GetField(nameof(__CSharpRepl_RuntimeHelper.SpanOutput.SpanWasReadOnly))?.GetValue(value) is bool readOnly)
124160
{
125-
disposable.Dispose();
161+
type = (readOnly ? typeof(ReadOnlySpan<>) : typeof(Span<>)).MakeGenericType(array.GetType().GetElementType() ?? array.GetType());
126162
}
127163
}
128164

129-
return new FormattedObjectRenderable(
130-
new RenderableSequence(header, table, separateByLineBreak: true),
131-
renderOnNewLine: false);
132-
}
133-
134-
private static void AppendHeader(StyledStringBuilder sb, IEnumerable value, Formatter formatter)
135-
{
136165
var isArray = value is Array;
137-
sb.Append(
138-
formatter.FormatTypeName(
139-
isArray ? (value.GetType().GetElementType() ?? value.GetType()) : value.GetType(),
140-
showNamespaces: false,
141-
useLanguageKeywords: true,
142-
hideSystemNamespace: true));
143-
144-
if (TryGetCount(value, formatter, out var count))
166+
AppendTypeName(isArray);
167+
return true;
168+
169+
void AppendTypeName(bool isArray)
145170
{
146-
sb.Append(isArray ? '[' : '(');
147-
sb.Append(count);
148-
sb.Append(isArray ? ']' : ')');
171+
sb.Append(
172+
formatter.FormatTypeName(
173+
isArray ? (type.GetElementType() ?? type) : type,
174+
showNamespaces: false,
175+
useLanguageKeywords: true,
176+
hideSystemNamespace: true));
177+
178+
if (TryGetCount(value, formatter, out var count))
179+
{
180+
sb.Append(isArray ? '[' : '(');
181+
sb.Append(count);
182+
sb.Append(isArray ? ']' : ')');
183+
}
149184
}
150185
}
151186

CSharpRepl.Services/Roslyn/RoslynServices.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.Diagnostics.CodeAnalysis;
9+
using System.IO;
910
using System.Linq;
11+
using System.Reflection;
1012
using System.Threading;
1113
using System.Threading.Tasks;
1214
using CSharpRepl.Services.Completion;
@@ -113,7 +115,7 @@ public async Task<EvaluationResult> EvaluateAsync(string input, string[]? args =
113115
//each RunCompilation (modifies script state) and UpdateCurrentDocument (changes CurrentDocument) cannot be run concurrently
114116
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
115117

116-
input = input.Trim();
118+
input = await InterceptInputWhenRefStructReturnNeedsToBeHandled(input.Trim(), cancellationToken).ConfigureAwait(false);
117119
EvaluatingInput?.Invoke(input);
118120

119121
var result = await scriptRunner
@@ -427,7 +429,22 @@ public Task WarmUpAsync(string[] args) =>
427429

428430
logger.Log("Warm-up Starting");
429431

430-
var evaluationTask = EvaluateAsync(@"_ = ""REPL Warmup""", args);
432+
const string RuntimeHelperName = "CSharpRepl.Services.RuntimeHelper.cs";
433+
Task evaluationTask;
434+
using (var runtimeHelperStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(RuntimeHelperName))
435+
{
436+
if (runtimeHelperStream != null)
437+
{
438+
var runtimeHelperText = new StreamReader(runtimeHelperStream).ReadToEnd();
439+
evaluationTask = EvaluateAsync(runtimeHelperText, args);
440+
}
441+
else
442+
{
443+
evaluationTask = Task.CompletedTask;
444+
console.WriteErrorLine($"Unable to load '{RuntimeHelperName}'");
445+
}
446+
}
447+
431448
var highlightTask = SyntaxHighlightAsync(@"_ = ""REPL Warmup""");
432449
var formattingTask = FormatInput(@"_=""REPL Warmup"";", caret: 16, formatParentNodeOnly: false, default);
433450
var completionTask = Task.WhenAny(
@@ -440,4 +457,32 @@ public Task WarmUpAsync(string[] args) =>
440457
await Task.WhenAll(evaluationTask, highlightTask, formattingTask, completionTask).ConfigureAwait(false);
441458
logger.Log("Warm-up Complete");
442459
});
443-
}
460+
461+
private async Task<string> InterceptInputWhenRefStructReturnNeedsToBeHandled(string input, CancellationToken cancellationToken)
462+
{
463+
await Initialization.ConfigureAwait(false);
464+
465+
if ((await scriptRunner.HasValueReturningStatement(input, cancellationToken).ConfigureAwait(false)).TryGet(out var result) &&
466+
result.Type.IsRefLikeType)
467+
{
468+
if (result.Type is { Name: "Span" or "ReadOnlySpan", ContainingNamespace.Name: "System" })
469+
{
470+
var root = result.Expression.SyntaxTree.GetRoot(cancellationToken);
471+
var wrappedExpression =
472+
SyntaxFactory.InvocationExpression(
473+
SyntaxFactory.MemberAccessExpression(
474+
SyntaxKind.SimpleMemberAccessExpression,
475+
SyntaxFactory.IdentifierName(nameof(__CSharpRepl_RuntimeHelper)),
476+
SyntaxFactory.IdentifierName(nameof(__CSharpRepl_RuntimeHelper.HandleSpanOutput))))
477+
.WithArgumentList(
478+
SyntaxFactory.ArgumentList(
479+
SyntaxFactory.SingletonSeparatedList(
480+
SyntaxFactory.Argument(
481+
result.Expression))));
482+
root = root.ReplaceNode(result.Expression, wrappedExpression);
483+
return root.GetText().ToString();
484+
}
485+
}
486+
return input;
487+
}
488+
}

CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public Compilation CompileTransient(string code, OptimizationLevel optimizationL
123123

124124
private async Task<EvaluationResult.Success> CreateSuccessfulResult(string text, ScriptState<object> state, CancellationToken cancellationToken)
125125
{
126-
var hasValueReturningStatement = await HasValueReturningStatement(text, cancellationToken).ConfigureAwait(false);
126+
var hasValueReturningStatement = (await HasValueReturningStatement(text, cancellationToken).ConfigureAwait(false)).HasValue;
127127

128128
referenceAssemblyService.AddImplementationAssemblyReferences(state.Script.GetCompilation().References);
129129
var frameworkReferenceAssemblies = referenceAssemblyService.LoadedReferenceAssemblies;
@@ -145,7 +145,7 @@ private async Task<ScriptState<object>> EvaluateStringWithStateAsync(string text
145145
return await scriptTask.ConfigureAwait(false);
146146
}
147147

148-
private async Task<bool> HasValueReturningStatement(string text, CancellationToken cancellationToken)
148+
internal async Task<(ExpressionSyntax Expression, ITypeSymbol Type)?> HasValueReturningStatement(string text, CancellationToken cancellationToken)
149149
{
150150
var sourceText = SourceText.From(text);
151151
var document = workspaceManager.CurrentDocument.WithText(sourceText);
@@ -160,11 +160,14 @@ await tree.GetRootAsync(cancellationToken).ConfigureAwait(false) is CompilationU
160160
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
161161
if (semanticModel != null)
162162
{
163-
var typeInfo = semanticModel.GetTypeInfo(possiblyValueReturningStatement.Expression, cancellationToken);
164-
return typeInfo.ConvertedType?.SpecialType != SpecialType.System_Void;
163+
var returnType = semanticModel.GetTypeInfo(possiblyValueReturningStatement.Expression, cancellationToken).ConvertedType;
164+
if (returnType?.SpecialType is not (null or SpecialType.System_Void))
165+
{
166+
return (possiblyValueReturningStatement.Expression, returnType);
167+
}
165168
}
166169
}
167-
return false;
170+
return null;
168171
}
169172

170173
private ScriptGlobals CreateGlobalsObject(string[]? args)

0 commit comments

Comments
 (0)