From deb67598bfaeee0d97af8c7153e6952cbe5421b0 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Mon, 25 Dec 2023 08:32:33 -0600 Subject: [PATCH] Cleaning up indentation on raw string literals (#977) * This isn't correct, but I wanna see what 3rd party code does closes #975 * Getting a basic version of this working * Handling everything except for tabs * format files * Fixing issues with tabs + passing options around * self code review * more tweaks * formatting files --------- Co-authored-by: Lasath Fernando --- Src/CSharpier.Tests/CSharpierIgnoreTests.cs | 7 +++- .../TestFiles/cs/RawStringLiterals.test | 27 +++++++++++++ ...awStringLiterals_MovesIndent.expected.test | 3 ++ .../cs/RawStringLiterals_MovesIndent.test | 3 ++ ...ingLiterals_MovesIndent_Tabs.expected.test | 3 ++ .../RawStringLiterals_MovesIndent_Tabs.test | 3 ++ .../TestFiles/cs/RawStringLiterals_Tabs.test | 27 +++++++++++++ .../RawStringLiterals_TrimExtra.expected.test | 5 +++ .../cs/RawStringLiterals_TrimExtra.test | 5 +++ ...tringLiterals_TrimExtra_Tabs.expected.test | 5 +++ .../cs/RawStringLiterals_TrimExtra_Tabs.test | 5 +++ .../TestFiles/cs/StringLiterals.test | 14 +++---- Src/CSharpier/CSharpFormatter.cs | 14 ++++++- Src/CSharpier/DocPrinter/DocPrinter.cs | 29 ++++---------- Src/CSharpier/DocTypes/Doc.cs | 2 + Src/CSharpier/DocTypes/HardLineNoTrim.cs | 3 ++ .../SyntaxPrinter/FormattingContext.cs | 11 +++++- Src/CSharpier/SyntaxPrinter/Token.cs | 38 ++++++++++++++++++- Src/CSharpier/Utilities/StringExtensions.cs | 27 +++++++++++++ 19 files changed, 196 insertions(+), 35 deletions(-) create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.expected.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.expected.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_Tabs.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.expected.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.expected.test create mode 100644 Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.test create mode 100644 Src/CSharpier/DocTypes/HardLineNoTrim.cs diff --git a/Src/CSharpier.Tests/CSharpierIgnoreTests.cs b/Src/CSharpier.Tests/CSharpierIgnoreTests.cs index dc4e78326..c90560fba 100644 --- a/Src/CSharpier.Tests/CSharpierIgnoreTests.cs +++ b/Src/CSharpier.Tests/CSharpierIgnoreTests.cs @@ -69,7 +69,12 @@ private string PrintWithoutFormatting(string code) { return CSharpierIgnore.PrintWithoutFormatting( code, - new FormattingContext { LineEnding = Environment.NewLine } + new FormattingContext + { + LineEnding = Environment.NewLine, + IndentSize = 4, + UseTabs = false + } ); } } diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals.test new file mode 100644 index 000000000..f3a2d3072 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals.test @@ -0,0 +1,27 @@ +var someString = """ + Indent based on previous line + """; + +var someString = """ + Indent based on previous line + This should stay indented + """; + +var doNotIndentIfEndDelimiterIsAtZero = """ +Keep This + Where It +Is +"""; + +var someOneWantsThisMuchIndentation = """ + + + + +"""; + +var whatAboutWhiteSpace = """ + Four Spaces + + That last line is six + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.expected.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.expected.test new file mode 100644 index 000000000..f6b89a909 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.expected.test @@ -0,0 +1,3 @@ +var someString = """ + Indent based on previous line + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.test new file mode 100644 index 000000000..3a3f7ee31 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent.test @@ -0,0 +1,3 @@ +var someString = """ + Indent based on previous line + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.expected.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.expected.test new file mode 100644 index 000000000..f8d4c4fb0 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.expected.test @@ -0,0 +1,3 @@ +var someString = """ + Indent based on previous line + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.test new file mode 100644 index 000000000..f9d59e06c --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_MovesIndent_Tabs.test @@ -0,0 +1,3 @@ +var someString = """ + Indent based on previous line + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_Tabs.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_Tabs.test new file mode 100644 index 000000000..bdd36d4a9 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_Tabs.test @@ -0,0 +1,27 @@ +var someString = """ + Indent based on previous line + """; + +var someString = """ + Indent based on previous line + This should stay indented + """; + +var doNotIndentIfEndDelimiterIsAtZero = """ +Keep This + Where It +Is +"""; + +var someOneWantsThisMuchIndentation = """ + + + + +"""; + +var whatAboutWhiteSpace = """ + One Tab + + That last line is two tabs + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.expected.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.expected.test new file mode 100644 index 000000000..71d25e199 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.expected.test @@ -0,0 +1,5 @@ +var whatAboutWhiteSpace = """ + Four Spaces + + That last line is only two + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.test new file mode 100644 index 000000000..a901b3c05 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra.test @@ -0,0 +1,5 @@ +var whatAboutWhiteSpace = """ + Four Spaces + + That last line is only two + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.expected.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.expected.test new file mode 100644 index 000000000..38e2e2a36 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.expected.test @@ -0,0 +1,5 @@ +var whatAboutWhiteSpace = """ + One Tab + + That last line is one tab + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.test new file mode 100644 index 000000000..150a91f07 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/RawStringLiterals_TrimExtra_Tabs.test @@ -0,0 +1,5 @@ +var whatAboutWhiteSpace = """ + One Tab + + That last line is one tab + """; diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/StringLiterals.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/StringLiterals.test index ba810dedd..464dcee32 100644 --- a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/StringLiterals.test +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/StringLiterals.test @@ -91,13 +91,13 @@ four", ); var multiLineRaw = """ - This is a long message. - It has several lines. - Some are indented - more than others. - Some should start at the first column. - Some have "quoted text" in them. - """; + This is a long message. + It has several lines. + Some are indented + more than others. + Some should start at the first column. + Some have "quoted text" in them. + """; var multiLineRawInterpolated = $""" This is a long message. diff --git a/Src/CSharpier/CSharpFormatter.cs b/Src/CSharpier/CSharpFormatter.cs index 2ef245efa..f1fe2249a 100644 --- a/Src/CSharpier/CSharpFormatter.cs +++ b/Src/CSharpier/CSharpFormatter.cs @@ -104,7 +104,12 @@ bool TryGetCompilationFailure(out CodeFormatterResult compilationResult) try { var lineEnding = PrinterOptions.GetLineEnding(syntaxTree.ToString(), printerOptions); - var formattingContext = new FormattingContext { LineEnding = lineEnding }; + var formattingContext = new FormattingContext + { + LineEnding = lineEnding, + IndentSize = printerOptions.TabWidth, + UseTabs = printerOptions.UseTabs, + }; var document = Node.Print(rootNode, formattingContext); var formattedCode = DocPrinter.DocPrinter.Print(document, printerOptions, lineEnding); var reorderedModifiers = formattingContext.ReorderedModifiers; @@ -119,7 +124,12 @@ bool TryGetCompilationFailure(out CodeFormatterResult compilationResult) return result; } - var formattingContext2 = new FormattingContext { LineEnding = lineEnding }; + var formattingContext2 = new FormattingContext + { + LineEnding = lineEnding, + IndentSize = printerOptions.TabWidth, + UseTabs = printerOptions.UseTabs, + }; document = Node.Print( await syntaxTree.GetRootAsync(cancellationToken), formattingContext2 diff --git a/Src/CSharpier/DocPrinter/DocPrinter.cs b/Src/CSharpier/DocPrinter/DocPrinter.cs index eefc231f1..fd9991221 100644 --- a/Src/CSharpier/DocPrinter/DocPrinter.cs +++ b/Src/CSharpier/DocPrinter/DocPrinter.cs @@ -179,27 +179,8 @@ private void ProcessNextCommand() private void AppendComment(LeadingComment leadingComment, Indent indent) { - int CalculateIndentLength(string line) - { - var result = 0; - foreach (var character in line) - { - if (character == ' ') - { - result += 1; - } - else if (character == '\t') - { - result += this.PrinterOptions.TabWidth; - } - else - { - break; - } - } - - return result; - } + int CalculateIndentLength(string line) => + line.CalculateCurrentLeadingIndentation(this.PrinterOptions.TabWidth); var stringReader = new StringReader(leadingComment.Comment); var line = stringReader.ReadLine(); @@ -315,7 +296,11 @@ private void ProcessLine(LineDoc line, PrintMode mode, Indent indent) { if (!this.SkipNextNewLine || !this.NewLineNextStringValue) { - this.Output.TrimTrailingWhitespace(); + if (line is not HardLineNoTrim) + { + this.Output.TrimTrailingWhitespace(); + } + this.Output.Append(this.EndOfLine).Append(indent.Value); this.CurrentWidth = indent.Length; } diff --git a/Src/CSharpier/DocTypes/Doc.cs b/Src/CSharpier/DocTypes/Doc.cs index 199cef6a3..e7e423732 100644 --- a/Src/CSharpier/DocTypes/Doc.cs +++ b/Src/CSharpier/DocTypes/Doc.cs @@ -23,6 +23,8 @@ public static implicit operator Doc(string value) public static readonly HardLine HardLine = new(); + public static readonly HardLineNoTrim HardLineNoTrim = new(); + public static readonly HardLine HardLineSkipBreakIfFirstInGroup = new(false, true); public static readonly HardLine HardLineIfNoPreviousLine = new(true); diff --git a/Src/CSharpier/DocTypes/HardLineNoTrim.cs b/Src/CSharpier/DocTypes/HardLineNoTrim.cs new file mode 100644 index 000000000..0427c6a68 --- /dev/null +++ b/Src/CSharpier/DocTypes/HardLineNoTrim.cs @@ -0,0 +1,3 @@ +namespace CSharpier.DocTypes; + +internal class HardLineNoTrim : HardLine { } diff --git a/Src/CSharpier/SyntaxPrinter/FormattingContext.cs b/Src/CSharpier/SyntaxPrinter/FormattingContext.cs index 6708314ce..3048fb89c 100644 --- a/Src/CSharpier/SyntaxPrinter/FormattingContext.cs +++ b/Src/CSharpier/SyntaxPrinter/FormattingContext.cs @@ -1,11 +1,20 @@ namespace CSharpier.SyntaxPrinter; +// TODO rename this to PrintingContext +// TODO and rename PrinterOptions.TabWidth to PrinterOptions.IndentSize internal class FormattingContext { + // TODO these go into Options + // context.Options.LineEnding + public required string LineEnding { get; init; } + public required int IndentSize { get; init; } + public required bool UseTabs { get; init; } + + // TODO the rest of these go into State + // context.State.PrintingDepth public int PrintingDepth { get; set; } public bool NextTriviaNeedsLine { get; set; } public bool ShouldSkipNextLeadingTrivia { get; set; } - public required string LineEnding { get; init; } // we need to keep track if we reordered modifiers because when modifiers are moved inside // of an #if, then we can't compare the before and after disabled text in the source file diff --git a/Src/CSharpier/SyntaxPrinter/Token.cs b/Src/CSharpier/SyntaxPrinter/Token.cs index d9da9e103..75e94f248 100644 --- a/Src/CSharpier/SyntaxPrinter/Token.cs +++ b/Src/CSharpier/SyntaxPrinter/Token.cs @@ -62,12 +62,46 @@ is InterpolatedStringExpressionSyntax { RawKind: (int)SyntaxKind.InterpolatedVerbatimStringStartToken } } ) - || syntaxToken.RawSyntaxKind() is SyntaxKind.MultiLineRawStringLiteralToken ) { - var lines = syntaxToken.Text.Replace("\r", string.Empty).Split(new[] { '\n' }); + var lines = syntaxToken.Text.Replace("\r", string.Empty).Split('\n'); docs.Add(Doc.Join(Doc.LiteralLine, lines.Select(o => new StringDoc(o)))); } + else if (syntaxToken.RawSyntaxKind() is SyntaxKind.MultiLineRawStringLiteralToken) + { + var contents = new List(); + var lines = syntaxToken.Text.Replace("\r", string.Empty).Split('\n'); + var currentIndentation = lines[^1].CalculateCurrentLeadingIndentation( + context.IndentSize + ); + if (currentIndentation == 0) + { + contents.Add(Doc.Join(Doc.LiteralLine, lines.Select(o => new StringDoc(o)))); + } + else + { + foreach (var line in lines) + { + var indentation = line.CalculateCurrentLeadingIndentation(context.IndentSize); + var numberOfSpacesToAddOrRemove = indentation - currentIndentation; + var modifiedLine = + numberOfSpacesToAddOrRemove > 0 + ? context.UseTabs + ? new string('\t', numberOfSpacesToAddOrRemove / context.IndentSize) + : new string(' ', numberOfSpacesToAddOrRemove) + : string.Empty; + modifiedLine += line.TrimStart(); + contents.Add(modifiedLine); + contents.Add( + numberOfSpacesToAddOrRemove > 0 ? Doc.HardLineNoTrim : Doc.HardLine + ); + } + + contents.RemoveAt(contents.Count - 1); + } + + docs.Add(Doc.Indent(contents)); + } else { docs.Add(syntaxToken.Text); diff --git a/Src/CSharpier/Utilities/StringExtensions.cs b/Src/CSharpier/Utilities/StringExtensions.cs index e3867287e..c4aaaf86f 100644 --- a/Src/CSharpier/Utilities/StringExtensions.cs +++ b/Src/CSharpier/Utilities/StringExtensions.cs @@ -42,4 +42,31 @@ public static int GetPrintedWidth(this string value) { return value.Length; } + + public static int CalculateCurrentLeadingIndentation(this string line, int indentSize) + { + var result = 0; + foreach (var character in line) + { + if (character == ' ') + { + result += 1; + } + // I'm not sure why this converts tabs to the size of an indent + // I'd think this should be based on if UseTabs is true or not + // if using tabs, this should be considered one + // but then how do we convert spaces to tabs? + // this seems to work, and it came from the comments code + else if (character == '\t') + { + result += indentSize; + } + else + { + break; + } + } + + return result; + } }