diff --git a/Src/CSharpier.Tests/DocPrinterTests.cs b/Src/CSharpier.Tests/DocPrinterTests.cs index 3df06b808..632995cf4 100644 --- a/Src/CSharpier.Tests/DocPrinterTests.cs +++ b/Src/CSharpier.Tests/DocPrinterTests.cs @@ -616,6 +616,26 @@ public void Conditional_Group_Does_Not_Propagate_Breaks_To_Parent() PrintedDocShouldBe(doc, "1 2", 10); } + [Test] + public void Align_Should_Print_Basic_Case() + { + var doc = Doc.Concat("+ ", Doc.Align(2, Doc.Group("1", Doc.HardLine, "2"))); + PrintedDocShouldBe(doc, $"+ 1{NewLine} 2"); + } + + [Test] + public void Align_Should_Convert_Spaces_To_Tabs() + { + var doc = Doc.Concat( + "+ ", + Doc.Align( + 2, + Doc.Indent(Doc.Concat("+ ", Doc.Align(2, Doc.Group("1", Doc.HardLine, "2")))) + ) + ); + PrintedDocShouldBe(doc, $"+ + 1{NewLine}\t\t 2", useTabs: true); + } + [Test] public void Scratch() { @@ -627,9 +647,10 @@ private static void PrintedDocShouldBe( Doc doc, string expected, int width = PrinterOptions.WidthUsedByTests, - bool trimInitialLines = false + bool trimInitialLines = false, + bool useTabs = false ) { - var result = Print(doc, width, trimInitialLines); + var result = Print(doc, width, trimInitialLines, useTabs); result.Should().Be(expected); } @@ -637,11 +658,17 @@ private static void PrintedDocShouldBe( private static string Print( Doc doc, int width = PrinterOptions.WidthUsedByTests, - bool trimInitialLines = false + bool trimInitialLines = false, + bool useTabs = false ) { return DocPrinter.DocPrinter.Print( doc, - new PrinterOptions { Width = width, TrimInitialLines = trimInitialLines, }, + new PrinterOptions + { + Width = width, + TrimInitialLines = trimInitialLines, + UseTabs = useTabs + }, Environment.NewLine ) .TrimEnd('\r', '\n'); diff --git a/Src/CSharpier.Tests/TestFiles/ClassDeclaration/ClassDeclarations.cst b/Src/CSharpier.Tests/TestFiles/ClassDeclaration/ClassDeclarations.cst index 135828c0a..d043add19 100644 --- a/Src/CSharpier.Tests/TestFiles/ClassDeclaration/ClassDeclarations.cst +++ b/Src/CSharpier.Tests/TestFiles/ClassDeclaration/ClassDeclarations.cst @@ -6,13 +6,13 @@ class NoModifiers { } public class WithInterface : IInterface { } -public class WithReallyLongNameInterface : - IReallyLongNameLetsMakeThisBreak___________________________ { } +public class WithReallyLongNameInterface + : IReallyLongNameLetsMakeThisBreak___________________________ { } -public class ThisIsSomeLongNameAndItShouldFormatWell1 : - AnotherLongClassName, - AndYetAnotherLongClassName, - AndStillOneMore { } +public class ThisIsSomeLongNameAndItShouldFormatWell1 + : AnotherLongClassName, + AndYetAnotherLongClassName, + AndStillOneMore { } public class SimpleGeneric where T : new() { } @@ -36,9 +36,24 @@ public class LongerClassNameWithLotsOfGenerics< public class SimpleGeneric : BaseClass where T : new() { } -public class ThisIsSomeLongNameAndItShouldFormatWell2 : - AnotherLongClassName, - AnotherClassName +public class ThisIsSomeLongNameAndItShouldFormatWell2 + : AnotherLongClassName, + AnotherClassName where T : new(), AnotherTypeConstraint where T2 : new() where T3 : new() { } + +public class IdentityDbContext + : IdentityDbContext< + TUser, + TRole, + TKey, + IdentityUserClaim, + IdentityUserRole, + IdentityUserLogin, + IdentityRoleClaim, + IdentityUserToken + > + where TUser : IdentityUser + where TRole : IdentityRole + where TKey : IEquatable { } diff --git a/Src/CSharpier.Tests/TestFiles/RecordDeclaration/RecordDeclarations.cst b/Src/CSharpier.Tests/TestFiles/RecordDeclaration/RecordDeclarations.cst index 209e1d82f..2d8466d7e 100644 --- a/Src/CSharpier.Tests/TestFiles/RecordDeclaration/RecordDeclarations.cst +++ b/Src/CSharpier.Tests/TestFiles/RecordDeclaration/RecordDeclarations.cst @@ -17,7 +17,7 @@ record PC(string x) : PrimaryConstructor(x) { } record RecordWithoutBody(string property); -record LongerRecordNameWhatHappens_________________________________________(string x) : - R4(x) { } +record LongerRecordNameWhatHappens_________________________________________(string x) + : R4(x) { } record GenericRecord(T Result); diff --git a/Src/CSharpier/DocPrinter/DocFitter.cs b/Src/CSharpier/DocPrinter/DocFitter.cs index 3488fa2fa..19abce2a1 100644 --- a/Src/CSharpier/DocPrinter/DocFitter.cs +++ b/Src/CSharpier/DocPrinter/DocFitter.cs @@ -83,7 +83,7 @@ void Push(Doc doc, PrintMode printMode, Indent indent) Push( indent.Contents, currentMode, - IndentBuilder.Make(currentIndent, printerOptions) + IndentBuilder.MakeIndent(currentIndent, printerOptions) ); break; case Trim: @@ -146,6 +146,17 @@ is ConditionalGroup conditionalGroup break; case BreakParent: break; + case Align align: + Push( + align.Contents, + currentMode, + IndentBuilder.MakeAlign( + currentIndent, + align.Alignment, + printerOptions + ) + ); + break; default: throw new Exception("Can't handle " + currentDoc.GetType()); } diff --git a/Src/CSharpier/DocPrinter/DocPrinter.cs b/Src/CSharpier/DocPrinter/DocPrinter.cs index 2e7d3d9ab..435635c38 100644 --- a/Src/CSharpier/DocPrinter/DocPrinter.cs +++ b/Src/CSharpier/DocPrinter/DocPrinter.cs @@ -78,7 +78,11 @@ private void ProcessNextCommand() break; } case IndentDoc indentDoc: - Push(indentDoc.Contents, mode, IndentBuilder.Make(indent, PrinterOptions)); + Push( + indentDoc.Contents, + mode, + IndentBuilder.MakeIndent(indent, PrinterOptions) + ); break; case Trim: CurrentWidth -= Output.TrimTrailingWhitespace(); @@ -128,6 +132,13 @@ private void ProcessNextCommand() case ForceFlat forceFlat: Push(forceFlat.Contents, PrintMode.Flat, indent); break; + case Align align: + Push( + align.Contents, + mode, + IndentBuilder.MakeAlign(indent, align.Alignment, PrinterOptions) + ); + break; default: throw new Exception("didn't handle " + doc); } diff --git a/Src/CSharpier/DocPrinter/Indent.cs b/Src/CSharpier/DocPrinter/Indent.cs index 00dcc0b83..aaaa85a33 100644 --- a/Src/CSharpier/DocPrinter/Indent.cs +++ b/Src/CSharpier/DocPrinter/Indent.cs @@ -4,33 +4,130 @@ namespace CSharpier.DocPrinter { - public record Indent(string Value, int Length); + public class Indent + { + public string Value = string.Empty; + public int Length; + public IList? TypesForTabs; + } + + public class IndentType + { + public bool IsAlign { get; set; } + public int AlignWidth { get; set; } + } public static class IndentBuilder { public static Indent MakeRoot() { - return new Indent(string.Empty, 0); + return new(); } - public static Indent Make(Indent indent, PrinterOptions printerOptions) + public static Indent MakeIndent(Indent indent, PrinterOptions printerOptions) { return GenerateIndent(indent, printerOptions); } private static Indent GenerateIndent(Indent indent, PrinterOptions printerOptions) + { + if (!printerOptions.UseTabs) + { + return new Indent + { + Value = indent.Value + new string(' ', printerOptions.TabWidth), + Length = indent.Length + printerOptions.TabWidth + }; + } + + if (indent.TypesForTabs != null) + { + return MakeIndentWithTypesForTabs(indent, new IndentType(), printerOptions); + } + + return new Indent + { + Value = indent.Value + "\t", + Length = indent.Length + printerOptions.TabWidth + }; + } + + public static Indent MakeAlign(Indent indent, int alignment, PrinterOptions printerOptions) { if (printerOptions.UseTabs) { - return new Indent(indent.Value + "\t", indent.Length + printerOptions.TabWidth); + return MakeIndentWithTypesForTabs( + indent, + new IndentType { IsAlign = true, AlignWidth = alignment }, + printerOptions + ); } else { - return new Indent( - indent.Value + new string(' ', printerOptions.TabWidth), - indent.Length + printerOptions.TabWidth - ); + return new Indent + { + Value = indent.Value + new string(' ', alignment), + Length = indent.Length + alignment + }; + } + } + + // when using tabs we need to sometimes replace the spaces from align with tabs + // trailing aligns stay as spaces, but any aligns before a tab get converted to a single tab + // see https://github.com/prettier/prettier/blob/main/commands.md#align + private static Indent MakeIndentWithTypesForTabs( + Indent indent, + IndentType nextIndentType, + PrinterOptions printerOptions + ) { + List types; + + // if it doesn't exist yet, then all values on it are regular indents, not aligns + if (indent.TypesForTabs == null) + { + types = new List(); + for (var x = 0; x < indent.Value.Length; x++) + { + types.Add(new IndentType()); + } } + else + { + var placeTab = false; + types = new List(indent.TypesForTabs); + for (var x = types.Count - 1; x >= 0; x--) + { + if (!types[x].IsAlign) + { + placeTab = true; + } + + if (placeTab) + { + types[x] = new IndentType(); + } + } + } + + types.Add(nextIndentType); + + var length = 0; + var value = new StringBuilder(); + foreach (var indentType in types) + { + if (indentType.IsAlign) + { + value.Append(' ', indentType.AlignWidth); + length += indentType.AlignWidth; + } + else + { + value.Append('\t'); + length += printerOptions.TabWidth; + } + } + + return new Indent { Length = length, Value = value.ToString(), TypesForTabs = types }; } } } diff --git a/Src/CSharpier/DocSerializer.cs b/Src/CSharpier/DocSerializer.cs index d21ed59ad..691482a0a 100644 --- a/Src/CSharpier/DocSerializer.cs +++ b/Src/CSharpier/DocSerializer.cs @@ -61,6 +61,7 @@ string PrintConcat(Concat concatToPrint) LineDoc lineDoc => indent + (lineDoc.Type == LineDoc.LineType.Normal ? "Doc.Line" : "Doc.SoftLine"), BreakParent => "", + Align align => $"{indent}Doc.Align({align.Alignment}, {PrintIndentedDocTree(align.Contents)})", Trim => $"{indent}Doc.Trim", ForceFlat forceFlat => $"{indent}Doc.ForceFlat({newLine}{PrintIndentedDocTree(forceFlat.Contents)})", IndentDoc indentDoc => $"{indent}Doc.Indent({newLine}{PrintIndentedDocTree(indentDoc.Contents)}{newLine}{indent})", diff --git a/Src/CSharpier/DocTypes/Align.cs b/Src/CSharpier/DocTypes/Align.cs new file mode 100644 index 000000000..d8126af22 --- /dev/null +++ b/Src/CSharpier/DocTypes/Align.cs @@ -0,0 +1,21 @@ +using System; + +namespace CSharpier.DocTypes +{ + public class Align : Doc + { + public int Alignment { get; } + public Doc Contents { get; } + + public Align(int alignment, Doc contents) + { + if (alignment < 1) + { + throw new Exception($"{nameof(alignment)} must be >= 1"); + } + + this.Alignment = alignment; + this.Contents = contents; + } + } +} diff --git a/Src/CSharpier/DocTypes/Doc.cs b/Src/CSharpier/DocTypes/Doc.cs index 13f754a9f..5f6b57e41 100644 --- a/Src/CSharpier/DocTypes/Doc.cs +++ b/Src/CSharpier/DocTypes/Doc.cs @@ -107,6 +107,8 @@ public static IndentIfBreak IndentIfBreak(Doc contents, string groupId) => public static Doc Directive(string value) => new StringDoc(value, true); public static ConditionalGroup ConditionalGroup(params Doc[] options) => new(options); + + public static Align Align(int alignment, Doc contents) => new(alignment, contents); } public enum CommentType diff --git a/Src/CSharpier/SyntaxPrinter/SyntaxNodePrinters/BaseList.cs b/Src/CSharpier/SyntaxPrinter/SyntaxNodePrinters/BaseList.cs index 9dfc1ca41..b6ff0639d 100644 --- a/Src/CSharpier/SyntaxPrinter/SyntaxNodePrinters/BaseList.cs +++ b/Src/CSharpier/SyntaxPrinter/SyntaxNodePrinters/BaseList.cs @@ -7,11 +7,15 @@ public static class BaseList { public static Doc Print(BaseListSyntax node) { - return Doc.Concat( - " ", - Token.Print(node.ColonToken), + return Doc.Group( Doc.Indent( - Doc.Group(Doc.Line, SeparatedSyntaxList.Print(node.Types, Node.Print, Doc.Line)) + Doc.Line, + Token.Print(node.ColonToken), + " ", + Doc.Align( + 2, + Doc.Concat(SeparatedSyntaxList.Print(node.Types, Node.Print, Doc.Line)) + ) ) ); }