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
7 changes: 7 additions & 0 deletions CodeBlocker.Test/SplitLinesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ private static string Split(string fragment, string newLine = NewLines.Lf)
[DataRow("c = '\nb", "c = '|b", DisplayName = "Unterminated char literal")]
[DataRow("s = \"p\nb", "s = \"p|b", DisplayName = "Unterminated regular string")]
[DataRow("/* @\"\nb", "/* @\"|b", DisplayName = "Unterminated block comment")]
[DataRow("#region a @\"C:\\\nint a;\n#endregion", "#region a @\"C:\\|int a;|#endregion", DisplayName = "Verbatim prefix in a #region name")]
[DataRow("\t #error $@\"x\nb", "\t #error $@\"x|b", DisplayName = "Interpolated verbatim prefix in an indented #error")]
[DataRow("#warning @$\"x\nb", "#warning @$\"x|b", DisplayName = "Verbatim interpolated prefix in a #warning")]
[DataRow("#pragma warning disable // @\"\nb", "#pragma warning disable // @\"|b", DisplayName = "Verbatim prefix in a #pragma comment")]
[DataRow("s = @\"p\n#q\";\nb", "s = @\"p\n#q\";|b", DisplayName = "Verbatim line starting with # stays in its literal")]
[DataRow("s = @\"p\n #region @\"\"q\";\nb", "s = @\"p\n #region @\"\"q\";|b", DisplayName = "Directive-like line inside a verbatim literal is literal text")]
[DataRow("a = b # @\"p\nq\";\nc", "a = b # @\"p\nq\";|c", DisplayName = "A # mid-line starts no directive")]
public void SplitsOnlyOutsideVerbatimStrings(string fragment, string expected) =>
Assert.AreEqual(expected, Split(fragment));

Expand Down
35 changes: 35 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ private static string Render(Action<CodeBlocker> write)

private static string Render(TemplateBase template) => Render(template.WriteTo);

[TestMethod]
public void RegionNameWithAVerbatimPrefixKeepsTheMemberIndented()
{
// ktsu-dev/CodeBlocker#194.
ClassTemplate template = new()
{
Name = "C",
Members =
{
new MethodTemplate
{
Type = "void",
Name = "M",
BodyFactory = b =>
{
b.WriteLine("{");
b.Indent();
using (new RegionScope(b, "Paths like @\"C:\\"))
{
b.WriteLine("int a = 1;");
b.WriteLine("int b = 2;");
}

b.Outdent();
b.WriteLine("}");
},
},
},
};

Assert.AreEqual(
"class C\n{\n\tvoid M()\n\t{\n\t\t#region Paths like @\"C:\\\n\t\tint a = 1;\n\t\tint b = 2;\n\t\t#endregion\n\t}\n}\n",
Render(template));
}

#region Blank entries

[TestMethod]
Expand Down
12 changes: 12 additions & 0 deletions CodeBlocker.Test/WriteLinesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ public void LeavesAVerbatimLiteralsLinesAsWritten()
Assert.AreEqual($"\tstring s = @\"first\r\nsecond\";{Nl}\tf();{Nl}", codeBlocker.ToString());
}

[TestMethod]
public void IndentsEveryLineAfterAVerbatimPrefixInADirective()
{
// ktsu-dev/CodeBlocker#194: the @" in the #region name opened a verbatim string that never
// closed, so the three lines after it were written at column 0.
using CodeBlocker codeBlocker = CodeBlocker.Create();
codeBlocker.Indent();
codeBlocker.WriteLines("#region Handles @\" prefixes\nint a;\nint b;\n#endregion\n");

Assert.AreEqual($"\t#region Handles @\" prefixes{Nl}\tint a;{Nl}\tint b;{Nl}\t#endregion{Nl}", codeBlocker.ToString());
}

[TestMethod]
public void KeepsBlankLinesFreeOfIndentation()
{
Expand Down
75 changes: 69 additions & 6 deletions CodeBlocker/Templates/VerbatimStringScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,10 @@ private static void ScanCode(string text, ref int position, bool[] inVerbatimTex
{
int braceDepth = 0;
int bracketDepth = 0;
bool atLineStart = !isInterpolationHole;
while (position < text.Length)
{
if (lineCommentStarts is not null && text[position] == '/' && At(text, position + 1) == '/')
{
lineCommentStarts.Add(position);
}

if (TrySkipCommentOrLiteral(text, ref position, inVerbatimText, inCommentText))
if (TrySkipNonCode(text, ref position, inVerbatimText, lineCommentStarts, inCommentText, ref atLineStart, isInterpolationHole))
{
continue;
}
Expand All @@ -118,6 +114,73 @@ private static void ScanCode(string text, ref int position, bool[] inVerbatimTex
}
}

/// <summary>
/// Moves past the preprocessor line, comment or literal that starts at
/// <paramref name="position"/>, if one does, recording a line comment's start on the way.
/// </summary>
/// <returns>True when something was skipped.</returns>
private static bool TrySkipNonCode(string text, ref int position, bool[] inVerbatimText, List<int>? lineCommentStarts, bool[]? inCommentText, ref bool atLineStart, bool isInterpolationHole)
{
if (TrySkipPreprocessorLine(text, ref position, ref atLineStart, isInterpolationHole))
{
return true;
}

if (lineCommentStarts is not null && text[position] == '/' && At(text, position + 1) == '/')
{
lineCommentStarts.Add(position);
}

if (!TrySkipCommentOrLiteral(text, ref position, inVerbatimText, inCommentText))
{
return false;
}

// A literal can span lines, but the code after it does not start a line.
atLineStart = false;
return true;
}

/// <summary>
/// Moves past the rest of a preprocessor line when <paramref name="position"/> is at its
/// <c>#</c>, and otherwise tracks whether the walk is still at the start of a line.
/// </summary>
/// <remarks>
/// The compiler does not tokenize the rest of a preprocessor line: the text after
/// <c>#region</c>, <c>#error</c> or <c>#warning</c> is free text, so an <c>@"</c> in it opens
/// nothing (ktsu-dev/CodeBlocker#194). The line break is left to the code around it. Only a
/// <c>#</c> preceded on its line by nothing but whitespace starts a directive, and never inside
/// an interpolation hole.
/// </remarks>
/// <param name="text">The C# fragment to scan.</param>
/// <param name="position">The current position, moved to the line break when a directive is skipped.</param>
/// <param name="atLineStart">Whether only whitespace has been seen since the last line break.</param>
/// <param name="isInterpolationHole">Whether the walk is inside an interpolation hole.</param>
/// <returns>True when a directive line was skipped.</returns>
private static bool TrySkipPreprocessorLine(string text, ref int position, ref bool atLineStart, bool isInterpolationHole)
{
char c = text[position];
if (IsLineBreak(c))
{
atLineStart = !isInterpolationHole;
return false;
}

if (!atLineStart)
{
return false;
}

if (c == '#')
{
SkipLineComment(text, ref position);
return true;
}

atLineStart = char.IsWhiteSpace(c);
return false;
}

/// <summary>
/// Tracks parenthesis and bracket nesting inside an interpolation hole, and reports whether the
/// character at <paramref name="position"/> is the colon that starts the hole's format specifier.
Expand Down
Loading