diff --git a/Src/CSharpier.Tests/DocPrinterTests.cs b/Src/CSharpier.Tests/DocPrinterTests.cs index 3df06b808..d43a368da 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_Non_Trailing_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..43653dbed 100644 --- a/Src/CSharpier/DocPrinter/DocFitter.cs +++ b/Src/CSharpier/DocPrinter/DocFitter.cs @@ -13,8 +13,8 @@ public static bool Fits( PrintCommand nextCommand, Stack remainingCommands, int remainingWidth, - PrinterOptions printerOptions, - Dictionary groupModeMap + Dictionary groupModeMap, + Indenter indenter ) { var returnFalseIfMoreStringsFound = false; var newCommands = new Stack(); @@ -83,7 +83,7 @@ void Push(Doc doc, PrintMode printMode, Indent indent) Push( indent.Contents, currentMode, - IndentBuilder.Make(currentIndent, printerOptions) + indenter.IncreaseIndent(currentIndent) ); break; case Trim: @@ -146,6 +146,13 @@ is ConditionalGroup conditionalGroup break; case BreakParent: break; + case Align align: + Push( + align.Contents, + currentMode, + indenter.AddAlign(currentIndent, align.Width) + ); + 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..5bb50670e 100644 --- a/Src/CSharpier/DocPrinter/DocPrinter.cs +++ b/Src/CSharpier/DocPrinter/DocPrinter.cs @@ -18,14 +18,14 @@ public class DocPrinter protected bool SkipNextNewLine; protected readonly string EndOfLine; protected readonly PrinterOptions PrinterOptions; + protected readonly Indenter Indenter; protected DocPrinter(Doc doc, PrinterOptions printerOptions, string endOfLine) { EndOfLine = endOfLine; PrinterOptions = printerOptions; - RemainingCommands.Push( - new PrintCommand(IndentBuilder.MakeRoot(), PrintMode.Break, doc) - ); + Indenter = new Indenter(printerOptions); + RemainingCommands.Push(new PrintCommand(Indenter.GenerateRoot(), PrintMode.Break, doc)); } public static string Print(Doc document, PrinterOptions printerOptions, string endOfLine) @@ -78,7 +78,7 @@ private void ProcessNextCommand() break; } case IndentDoc indentDoc: - Push(indentDoc.Contents, mode, IndentBuilder.Make(indent, PrinterOptions)); + Push(indentDoc.Contents, mode, Indenter.IncreaseIndent(indent)); break; case Trim: CurrentWidth -= Output.TrimTrailingWhitespace(); @@ -128,6 +128,9 @@ private void ProcessNextCommand() case ForceFlat forceFlat: Push(forceFlat.Contents, PrintMode.Flat, indent); break; + case Align align: + Push(align.Contents, mode, Indenter.AddAlign(indent, align.Width)); + break; default: throw new Exception("didn't handle " + doc); } @@ -271,8 +274,8 @@ private bool Fits(PrintCommand possibleCommand) possibleCommand, RemainingCommands, PrinterOptions.Width - CurrentWidth, - PrinterOptions, - GroupModeMap + GroupModeMap, + Indenter ); } diff --git a/Src/CSharpier/DocPrinter/Indent.cs b/Src/CSharpier/DocPrinter/Indent.cs index 00dcc0b83..a40558d98 100644 --- a/Src/CSharpier/DocPrinter/Indent.cs +++ b/Src/CSharpier/DocPrinter/Indent.cs @@ -1,36 +1,137 @@ -using System; using System.Collections.Generic; using System.Text; 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 interface IIndentType { } + + public class IndentType : IIndentType + { + protected IndentType() { } + + public static IndentType Instance = new(); + } + + public class AlignType : IIndentType + { + public int Width { get; init; } + } - public static class IndentBuilder + public class Indenter { - public static Indent MakeRoot() + protected readonly PrinterOptions PrinterOptions; + + public Indenter(PrinterOptions printerOptions) { - return new Indent(string.Empty, 0); + PrinterOptions = printerOptions; } - public static Indent Make(Indent indent, PrinterOptions printerOptions) + public Indent GenerateRoot() { - return GenerateIndent(indent, printerOptions); + return new(); } - private static Indent GenerateIndent(Indent indent, PrinterOptions printerOptions) + public Indent IncreaseIndent(Indent indent) { - if (printerOptions.UseTabs) + if (PrinterOptions.UseTabs) { - return new Indent(indent.Value + "\t", indent.Length + printerOptions.TabWidth); + if (indent.TypesForTabs != null) + { + return MakeIndentWithTypesForTabs(indent, IndentType.Instance); + } + + return new Indent + { + Value = indent.Value + "\t", + Length = indent.Length + PrinterOptions.TabWidth + }; } else { - return new Indent( - indent.Value + new string(' ', printerOptions.TabWidth), - indent.Length + printerOptions.TabWidth - ); + return new Indent + { + Value = indent.Value + new string(' ', PrinterOptions.TabWidth), + Length = indent.Length + PrinterOptions.TabWidth + }; } } + + public Indent AddAlign(Indent indent, int alignment) + { + if (PrinterOptions.UseTabs) + { + return MakeIndentWithTypesForTabs(indent, new AlignType { Width = alignment }); + } + else + { + 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 Indent MakeIndentWithTypesForTabs(Indent indent, IIndentType nextIndentType) + { + 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(IndentType.Instance); + } + } + else + { + var placeTab = false; + types = new List(indent.TypesForTabs); + for (var x = types.Count - 1; x >= 0; x--) + { + if (types[x] == IndentType.Instance) + { + placeTab = true; + } + + if (placeTab) + { + types[x] = IndentType.Instance; + } + } + } + + types.Add(nextIndentType); + + var length = 0; + var value = new StringBuilder(); + foreach (var indentType in types) + { + if (indentType is AlignType alignType) + { + value.Append(' ', alignType.Width); + length += alignType.Width; + } + 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..bfa480364 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.Width}, {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..881ddc4ea --- /dev/null +++ b/Src/CSharpier/DocTypes/Align.cs @@ -0,0 +1,21 @@ +using System; + +namespace CSharpier.DocTypes +{ + public class Align : Doc, IHasContents + { + public int Width { get; } + public Doc Contents { get; } + + public Align(int width, Doc contents) + { + if (width < 1) + { + throw new Exception($"{nameof(width)} must be >= 1"); + } + + this.Width = width; + this.Contents = contents; + } + } +} diff --git a/Src/CSharpier/DocTypes/Doc.cs b/Src/CSharpier/DocTypes/Doc.cs index 13f754a9f..50983d74f 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 @@ -117,6 +119,6 @@ public enum CommentType interface IHasContents { - Doc Contents { get; set; } + Doc Contents { get; } } } diff --git a/Src/CSharpier/DocTypes/StringDoc.cs b/Src/CSharpier/DocTypes/StringDoc.cs index 9a0dbeb3c..c77ef23f7 100644 --- a/Src/CSharpier/DocTypes/StringDoc.cs +++ b/Src/CSharpier/DocTypes/StringDoc.cs @@ -2,8 +2,8 @@ namespace CSharpier.DocTypes { public class StringDoc : Doc { - public string Value { get; set; } - public bool IsDirective { get; set; } + public string Value { get; } + public bool IsDirective { get; } public StringDoc(string value, bool isDirective = false) { diff --git a/Src/CSharpier/DocTypes/Trim.cs b/Src/CSharpier/DocTypes/Trim.cs index 9c0136079..67b0015a2 100644 --- a/Src/CSharpier/DocTypes/Trim.cs +++ b/Src/CSharpier/DocTypes/Trim.cs @@ -2,6 +2,5 @@ namespace CSharpier.DocTypes { public class Trim : Doc { - public Trim() { } } } 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)) + ) ) ); }