diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index b517a72..ce27649 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -228,6 +228,148 @@ public int Count Render(property)); } + [TestMethod] + public void AMultiLineExpressionBodiedPropertyKeepsTheEnclosingIndent() + { + ClassTemplate type = new() + { + Name = "C", + Keywords = { "public" }, + Members = + { + new PropertyTemplate + { + Type = "int", + Name = "X", + Keywords = { "public" }, + ExpressionBodyFactory = codeBlocker => + { + codeBlocker.WriteLine("k switch"); + codeBlocker.WriteLine("{"); + codeBlocker.Indent(); + codeBlocker.WriteLine("_ => 0,"); + codeBlocker.Outdent(); + codeBlocker.Write("}"); + }, + }, + }, + }; + + Assert.AreEqual( + """ + public class C + { + public int X => k switch + { + _ => 0, + }; + } + + """.ReplaceLineEndings("\n"), + Render(type)); + } + + [TestMethod] + public void AMultiLineExpressionBodiedAccessorKeepsTheEnclosingIndent() + { + ClassTemplate type = new() + { + Name = "C", + Keywords = { "public" }, + Members = + { + new PropertyTemplate + { + Type = "int[]", + Name = "Items", + Keywords = { "public" }, + Getter = AccessorTemplate.Expression(codeBlocker => + { + codeBlocker.WriteLine("["); + codeBlocker.Indent(); + codeBlocker.WriteLine("1,"); + codeBlocker.WriteLine("2,"); + codeBlocker.Outdent(); + codeBlocker.WriteLine("]"); + }), + }, + }, + }; + + Assert.AreEqual( + """ + public class C + { + public int[] Items + { + get => [ + 1, + 2, + ]; + } + } + + """.ReplaceLineEndings("\n"), + Render(type)); + } + + [TestMethod] + public void AnExpressionBodyEndingInALineTerminatorKeepsItsSemicolonOnTheLastLine() + { + PropertyTemplate property = new() + { + Type = "int", + Name = "Doubled", + Keywords = { "public" }, + ExpressionBodyFactory = codeBlocker => codeBlocker.WriteLine("count * 2"), + }; + + Assert.AreEqual("public int Doubled => count * 2;\n", Render(property)); + } + + [TestMethod] + public void AMultiLineExpressionBodyKeepsABlankLineWithoutIndentingIt() + { + ClassTemplate type = new() + { + Name = "C", + Keywords = { "public" }, + Members = + { + new PropertyTemplate + { + Type = "int", + Name = "X", + Keywords = { "public" }, + ExpressionBodyFactory = codeBlocker => + { + codeBlocker.WriteLine("a"); + codeBlocker.NewLine(); + codeBlocker.Write("+ b"); + }, + }, + }, + }; + + Assert.AreEqual( + "public class C\n{\n\tpublic int X => a\n\n\t+ b;\n}\n", + Render(type)); + } + + [TestMethod] + public void AnEmptyExpressionBodyStillTerminatesTheDeclaration() + { + PropertyTemplate property = new() + { + Type = "int", + Name = "X", + Keywords = { "public" }, + ExpressionBodyFactory = _ => { }, + }; + + Assert.AreEqual("public int X => ;\n", Render(property)); + } + [TestMethod] public void ABlockBodiedAccessorIsBracedAndIndented() { diff --git a/CodeBlocker/Templates/AccessorTemplate.cs b/CodeBlocker/Templates/AccessorTemplate.cs index 5e201d2..f941900 100644 --- a/CodeBlocker/Templates/AccessorTemplate.cs +++ b/CodeBlocker/Templates/AccessorTemplate.cs @@ -91,9 +91,7 @@ public void WriteTo(CodeBlocker codeBlocker, string keyword) case AccessorKind.Expression: codeBlocker.Write(prefix); - codeBlocker.Write(" => "); - codeBlocker.Write(TemplateRendering.RenderFragment(codeBlocker, BodyFactory)); - codeBlocker.WriteLine(";"); + TemplateRendering.WriteExpressionBody(codeBlocker, BodyFactory); break; case AccessorKind.Block: diff --git a/CodeBlocker/Templates/PropertyTemplate.cs b/CodeBlocker/Templates/PropertyTemplate.cs index feebaa5..87260ef 100644 --- a/CodeBlocker/Templates/PropertyTemplate.cs +++ b/CodeBlocker/Templates/PropertyTemplate.cs @@ -52,9 +52,7 @@ public override void WriteTo(CodeBlocker codeBlocker) if (ExpressionBodyFactory is not null) { - codeBlocker.Write(" => "); - codeBlocker.Write(TemplateRendering.RenderFragment(codeBlocker, ExpressionBodyFactory)); - codeBlocker.WriteLine(";"); + TemplateRendering.WriteExpressionBody(codeBlocker, ExpressionBodyFactory); return; } diff --git a/CodeBlocker/Templates/TemplateRendering.cs b/CodeBlocker/Templates/TemplateRendering.cs index 3fe9451..533efac 100644 --- a/CodeBlocker/Templates/TemplateRendering.cs +++ b/CodeBlocker/Templates/TemplateRendering.cs @@ -245,4 +245,53 @@ internal static void WriteBody(CodeBlocker codeBlocker, Action? bod codeBlocker.WriteLine(); SpliceFragment(codeBlocker, body); } + + /// + /// Writes an expression body — the arrow, the expression and its terminating semicolon — and + /// ends the line. + /// + /// The to write to. + /// + /// The callback that writes the expression, without => or a semicolon. + /// + /// + /// The first line of the expression stays on the declaration line, after the arrow, and every + /// following line is spliced at the current indent, so a multi-line switch expression, collection + /// expression or initialiser keeps the nesting it was written with. The semicolon goes on the last + /// line that has content, so a factory that ends with a line terminator does not leave it on a + /// line of its own. + /// + internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action? expressionFactory) + { + string[] lines = SplitLines(codeBlocker, RenderFragment(codeBlocker, expressionFactory)); + + int last = lines.Length - 1; + while (last >= 0 && lines[last].Length == 0) + { + last--; + } + + codeBlocker.Write(" => "); + if (last < 0) + { + codeBlocker.WriteLine(";"); + return; + } + + for (int i = 0; i <= last; i++) + { + if (i == last) + { + codeBlocker.WriteLine(lines[i] + ";"); + } + else if (lines[i].Length == 0) + { + codeBlocker.NewLine(); + } + else + { + codeBlocker.WriteLine(lines[i]); + } + } + } }