Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 42 additions & 21 deletions src/TUnit.Core.SourceGenerator/CodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,35 +215,56 @@ public ICodeWriter AppendRaw(string multilineText)
return this;
}

var lines = multilineText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None);

// Skip leading empty lines
var startIndex = 0;
while (startIndex < lines.Length && string.IsNullOrWhiteSpace(lines[startIndex]))
var position = 0;
var hasContent = false;
var pendingBlankLines = 0;
while (position < multilineText.Length)
{
startIndex++;
}
var lineStart = position;
while (position < multilineText.Length && multilineText[position] is not '\r' and not '\n')
{
position++;
}

// Skip trailing empty lines
var endIndex = lines.Length - 1;
while (endIndex >= startIndex && string.IsNullOrWhiteSpace(lines[endIndex]))
{
endIndex--;
}
var lineEnd = position;
if (position < multilineText.Length)
{
var newline = multilineText[position++];
if (newline == '\r' && position < multilineText.Length && multilineText[position] == '\n')
{
position++;
}
}

// Process remaining lines, preserving blank lines within the content
for (var i = startIndex; i <= endIndex; i++)
{
var line = lines[i].TrimEnd();
if (line.Length == 0)
while (lineEnd > lineStart && char.IsWhiteSpace(multilineText[lineEnd - 1]))
{
lineEnd--;
}

if (lineEnd == lineStart)
{
if (hasContent)
{
pendingBlankLines++;
}
continue;
}

// Delay blank lines so leading/trailing ones are omitted, while interior ones survive.
while (pendingBlankLines > 0)
{
// Preserve blank lines by forcing a newline even when already at line start
_builder.AppendLine();
pendingBlankLines--;
}
else

if (_isNewLine)
{
AppendLine(line);
_builder.Append(GetIndentation(_indentLevel));
}
_builder.Append(multilineText, lineStart, lineEnd - lineStart);
_builder.AppendLine();
_isNewLine = true;
hasContent = true;
}

return this;
Expand Down
52 changes: 52 additions & 0 deletions tests/TUnit.Core.SourceGenerator.Tests/CodeWriterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace TUnit.Core.SourceGenerator.Tests;

public class CodeWriterTests
{
[Test]
[Arguments("", "")]
[Arguments(" \t\r\n\u00a0\r\u2003\n", "")]
[Arguments("first", " first\n")]
[Arguments("first\nsecond", " first\n second\n")]
[Arguments("first\rsecond", " first\n second\n")]
[Arguments("first\r\nsecond", " first\n second\n")]
[Arguments("\r\n \t\n first \t\r\n\r\n\t\rsecond\n \t\n", " first\n\n\n second\n")]
[Arguments("first\u00a0\u2003\r\n\u00a0\nsecond\u0085", " first\n\n second\n")]
[Arguments("first\r\r\nsecond\n\n", " first\n\n second\n")]
[Arguments("first\u2028middle\nsecond", " first\u2028middle\n second\n")]
public async Task AppendRaw_PreservesFormatting(string input, string expected)
{
var writer = new CodeWriter(includeHeader: false);
writer.Indent();
writer.AppendRaw(input);
writer.Append("tail");

await Assert.That(writer.ToString()).IsEqualTo(expected.Replace("\n", Environment.NewLine) + " tail");
}

[Test]
public async Task AppendRaw_PreservesPartialLineAndSubsequentWrites()
{
var writer = new CodeWriter(indentString: "--", includeHeader: false);
writer.Indent();
writer.Append("prefix:");
writer.AppendRaw("\n \r\nfirst \t\r\n\t\n second\r\n\n");
writer.AppendRaw("third\n");
writer.Append("tail");

await Assert.That(writer.ToString()).IsEqualTo(
"--prefix:first\n\n-- second\n--third\n--tail".Replace("\n", Environment.NewLine));
}

[Test]
public async Task AppendRaw_EmptyInputDoesNotEndPartialLine()
{
var writer = new CodeWriter(includeHeader: false);
writer.Append("prefix:");
writer.AppendRaw(null!);
writer.AppendRaw("");
writer.AppendRaw(" \t\r\n\u00a0\n");
writer.Append("tail");

await Assert.That(writer.ToString()).IsEqualTo("prefix:tail");
}
}
Loading