From 2ca0bab22ab40e89dd7873b6e91165bde021136b Mon Sep 17 00:00:00 2001 From: Bastiaan Veelo Date: Mon, 30 May 2022 01:51:41 +0200 Subject: [PATCH 1/4] Retain line ending by default. If no `--end_of_line` was specified, find the first line ending in the input and use that for the whole output. All tests include checking for differences in line endings. Closes #552. --- .gitattributes | 12 ++++++++++ README.md | 2 +- src/dfmt/config.d | 6 ++--- src/dfmt/editorconfig.d | 9 +++---- src/dfmt/formatter.d | 21 ++++++++++++++-- src/dfmt/main.d | 4 +++- tests/allman/issue0552_cr.d.ref | 1 + tests/allman/issue0552_crlf.d.ref | 4 ++++ tests/allman/issue0552_lf.d.ref | 4 ++++ tests/issue0552_cr.d | 1 + tests/issue0552_crlf.d | 4 ++++ tests/issue0552_lf.d | 4 ++++ tests/knr/issue0552_cr.d.ref | 1 + tests/knr/issue0552_crlf.d.ref | 4 ++++ tests/knr/issue0552_lf.d.ref | 4 ++++ tests/otbs/issue0552_cr.d.ref | 1 + tests/otbs/issue0552_crlf.d.ref | 3 +++ tests/otbs/issue0552_lf.d.ref | 3 +++ tests/test.d | 40 ++----------------------------- 19 files changed, 79 insertions(+), 49 deletions(-) create mode 100644 tests/allman/issue0552_cr.d.ref create mode 100644 tests/allman/issue0552_crlf.d.ref create mode 100644 tests/allman/issue0552_lf.d.ref create mode 100644 tests/issue0552_cr.d create mode 100644 tests/issue0552_crlf.d create mode 100644 tests/issue0552_lf.d create mode 100644 tests/knr/issue0552_cr.d.ref create mode 100644 tests/knr/issue0552_crlf.d.ref create mode 100644 tests/knr/issue0552_lf.d.ref create mode 100644 tests/otbs/issue0552_cr.d.ref create mode 100644 tests/otbs/issue0552_crlf.d.ref create mode 100644 tests/otbs/issue0552_lf.d.ref diff --git a/.gitattributes b/.gitattributes index f961279c..59583777 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,15 @@ tests/issue0228.d text eol=lf tests/allman/issue0228.d.ref text eol=crlf tests/knr/issue0228.d.ref text eol=crlf tests/otbs/issue0228.d.ref text eol=crlf +tests/issue0552_lf.d text eol=lf +tests/allman/issue0552_lf.d.ref text eol=lf +tests/knr/issue0552_lf.d.ref text eol=lf +tests/otbs/issue0552_lf.d.ref text eol=lf +tests/issue0552_cr.d text eol=cr +tests/allman/issue0552_cr.d.ref text eol=cr +tests/knr/issue0552_cr.d.ref text eol=cr +tests/otbs/issue0552_cr.d.ref text eol=cr +tests/issue0552_crlf.d text eol=crlf +tests/allman/issue0552_crlf.d.ref text eol=crlf +tests/knr/issue0552_crlf.d.ref text eol=crlf +tests/otbs/issue0552_crlf.d.ref text eol=crlf diff --git a/README.md b/README.md index 822d70ee..b5fae366 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ void main(string[] args) ### Standard EditorConfig properties Property Name | Allowed Values | Description --------------|----------------|------------ -end_of_line | `cr`, `crlf` and **`lf`** | [See EditorConfig documentation.](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#end_of_line) +end_of_line | `cr`, `crlf` and `lf` | [See EditorConfig documentation.](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#end_of_line) When not set, `dfmt` adopts the first line ending in the input. insert_final_newline | **`true`** | Not supported. `dfmt` always inserts a final newline. charset | **`UTF-8`** | Not supported. `dfmt` only works correctly on UTF-8. indent_style | `tab`, **`space`** | [See EditorConfig documentation.](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#indent_style) diff --git a/src/dfmt/config.d b/src/dfmt/config.d index b2e49ed5..d6f31b78 100644 --- a/src/dfmt/config.d +++ b/src/dfmt/config.d @@ -10,7 +10,7 @@ import dfmt.editorconfig; /// Brace styles enum BraceStyle { - unspecified, + _unspecified, /// $(LINK https://en.wikipedia.org/wiki/Indent_style#Allman_style) allman, /// $(LINK https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS) @@ -23,7 +23,7 @@ enum BraceStyle enum TemplateConstraintStyle { - unspecified, + _unspecified, conditional_newline_indent, conditional_newline, always_newline, @@ -73,7 +73,7 @@ struct Config void initializeWithDefaults() { pattern = "*.d"; - end_of_line = EOL.lf; + end_of_line = EOL._default; indent_style = IndentStyle.space; indent_size = 4; tab_width = 4; diff --git a/src/dfmt/editorconfig.d b/src/dfmt/editorconfig.d index 205b9a44..ce79f7c0 100644 --- a/src/dfmt/editorconfig.d +++ b/src/dfmt/editorconfig.d @@ -26,21 +26,22 @@ private auto commentRe = ctRegex!(`^\s*[#;].*$`); enum OptionalBoolean : ubyte { - unspecified = 3, + _unspecified = 3, t = 1, f = 0 } enum IndentStyle : ubyte { - unspecified, + _unspecified, tab, space } enum EOL : ubyte { - unspecified, + _unspecified, + _default, lf, cr, crlf @@ -74,7 +75,7 @@ mixin template StandardEditorConfigFields() static if (N == "pattern") continue; else static if (is(T == enum)) - *thisN = otherN != T.unspecified ? otherN : *thisN; + *thisN = otherN != T._unspecified ? otherN : *thisN; else static if (is(T == int)) *thisN = otherN != -1 ? otherN : *thisN; else static if (is(T == string)) diff --git a/src/dfmt/formatter.d b/src/dfmt/formatter.d index 94333a86..c64ee9b7 100644 --- a/src/dfmt/formatter.d +++ b/src/dfmt/formatter.d @@ -105,6 +105,8 @@ struct TokenFormatter(OutputRange) this(const ubyte[] rawSource, const(Token)[] tokens, immutable short[] depths, OutputRange output, ASTInformation* astInformation, Config* config) { + import std.algorithm.searching : countUntil; + this.rawSource = rawSource; this.tokens = tokens; this.depths = depths; @@ -121,8 +123,23 @@ struct TokenFormatter(OutputRange) this.eolString = "\n"; else if (eol == eol.crlf) this.eolString = "\r\n"; - else if (eol == eol.unspecified) + else if (eol == eol._unspecified) assert(false, "config.end_of_line was unspecified"); + else + { + assert (eol == eol._default); // Same as input. + // Intentional wraparound, -1 turns into uint.max when not found: + const firstCR = cast(uint) rawSource.countUntil("\r"); + if (firstCR < cast(uint) rawSource.countUntil("\n")) + { + if (firstCR == rawSource.countUntil("\r\n")) + this.eolString = "\r\n"; + else + this.eolString = "\r"; + } + else + this.eolString = "\n"; + } } } @@ -371,7 +388,7 @@ private: import dfmt.editorconfig : OB = OptionalBoolean; with (TemplateConstraintStyle) final switch (config.dfmt_template_constraint_style) { - case unspecified: + case _unspecified: assert(false, "Config was not validated properly"); case conditional_newline: immutable l = currentLineLength + betweenParenLength(tokens[index + 1 .. $]); diff --git a/src/dfmt/main.d b/src/dfmt/main.d index 7a4d843f..316d8a2b 100644 --- a/src/dfmt/main.d +++ b/src/dfmt/main.d @@ -289,12 +289,14 @@ private version (Windows) template optionsToString(E) if (is(E == enum)) { + import std.algorithm.searching : startsWith; + enum optionsToString = () { string result = "("; foreach (s; [__traits(allMembers, E)]) { - if (s != "unspecified") + if (!s.startsWith("_")) result ~= s ~ "|"; } result = result[0 .. $ - 1] ~ ")"; diff --git a/tests/allman/issue0552_cr.d.ref b/tests/allman/issue0552_cr.d.ref new file mode 100644 index 00000000..e776d112 --- /dev/null +++ b/tests/allman/issue0552_cr.d.ref @@ -0,0 +1 @@ +/// Testing CR line endings. void main() { } \ No newline at end of file diff --git a/tests/allman/issue0552_crlf.d.ref b/tests/allman/issue0552_crlf.d.ref new file mode 100644 index 00000000..f226e538 --- /dev/null +++ b/tests/allman/issue0552_crlf.d.ref @@ -0,0 +1,4 @@ +/// Testing CRLF line endings. +void main() +{ +} diff --git a/tests/allman/issue0552_lf.d.ref b/tests/allman/issue0552_lf.d.ref new file mode 100644 index 00000000..ca5d2acc --- /dev/null +++ b/tests/allman/issue0552_lf.d.ref @@ -0,0 +1,4 @@ +/// Testing LF line endings. +void main() +{ +} diff --git a/tests/issue0552_cr.d b/tests/issue0552_cr.d new file mode 100644 index 00000000..e776d112 --- /dev/null +++ b/tests/issue0552_cr.d @@ -0,0 +1 @@ +/// Testing CR line endings. void main() { } \ No newline at end of file diff --git a/tests/issue0552_crlf.d b/tests/issue0552_crlf.d new file mode 100644 index 00000000..f226e538 --- /dev/null +++ b/tests/issue0552_crlf.d @@ -0,0 +1,4 @@ +/// Testing CRLF line endings. +void main() +{ +} diff --git a/tests/issue0552_lf.d b/tests/issue0552_lf.d new file mode 100644 index 00000000..ca5d2acc --- /dev/null +++ b/tests/issue0552_lf.d @@ -0,0 +1,4 @@ +/// Testing LF line endings. +void main() +{ +} diff --git a/tests/knr/issue0552_cr.d.ref b/tests/knr/issue0552_cr.d.ref new file mode 100644 index 00000000..e776d112 --- /dev/null +++ b/tests/knr/issue0552_cr.d.ref @@ -0,0 +1 @@ +/// Testing CR line endings. void main() { } \ No newline at end of file diff --git a/tests/knr/issue0552_crlf.d.ref b/tests/knr/issue0552_crlf.d.ref new file mode 100644 index 00000000..f226e538 --- /dev/null +++ b/tests/knr/issue0552_crlf.d.ref @@ -0,0 +1,4 @@ +/// Testing CRLF line endings. +void main() +{ +} diff --git a/tests/knr/issue0552_lf.d.ref b/tests/knr/issue0552_lf.d.ref new file mode 100644 index 00000000..ca5d2acc --- /dev/null +++ b/tests/knr/issue0552_lf.d.ref @@ -0,0 +1,4 @@ +/// Testing LF line endings. +void main() +{ +} diff --git a/tests/otbs/issue0552_cr.d.ref b/tests/otbs/issue0552_cr.d.ref new file mode 100644 index 00000000..246eac22 --- /dev/null +++ b/tests/otbs/issue0552_cr.d.ref @@ -0,0 +1 @@ +/// Testing CR line endings. void main() { } \ No newline at end of file diff --git a/tests/otbs/issue0552_crlf.d.ref b/tests/otbs/issue0552_crlf.d.ref new file mode 100644 index 00000000..d3d01c6a --- /dev/null +++ b/tests/otbs/issue0552_crlf.d.ref @@ -0,0 +1,3 @@ +/// Testing CRLF line endings. +void main() { +} diff --git a/tests/otbs/issue0552_lf.d.ref b/tests/otbs/issue0552_lf.d.ref new file mode 100644 index 00000000..0856a1e6 --- /dev/null +++ b/tests/otbs/issue0552_lf.d.ref @@ -0,0 +1,3 @@ +/// Testing LF line endings. +void main() { +} diff --git a/tests/test.d b/tests/test.d index 43f50d14..9310a2ad 100755 --- a/tests/test.d +++ b/tests/test.d @@ -29,13 +29,10 @@ int main() if (const result = spawnProcess(dfmtCommand, stdin, File(outFileName, "w")).wait) return result; - // As long as dfmt defaults to LF line endings (issue #552), we'll have to default to ignore - // the line endings in our verification with the reference. - const keepTerminator = dfmtCommand.any!(a => a.canFind("--end_of_line")).to!(Flag!"keepTerminator"); const outText = outFileName.readText; const refText = refFileName.readText; - const outLines = outText.splitLines(keepTerminator); - const refLines = refText.splitLines(keepTerminator); + const outLines = outText.splitLines(Yes.keepTerminator); + const refLines = refText.splitLines(Yes.keepTerminator); foreach (i; 0 .. min(refLines.length, outLines.length)) if (outLines[i] != refLines[i]) { @@ -56,26 +53,6 @@ int main() writefln("%(%s%)", [outLines[refLines.length]]); return 1; } - - // As long as dfmt defaults to LF line endings (issue #552) we need an explicit trailing newline check. - // because a) splitLines gives the same number of lines regardless whether the last line ends with a newline, - // and b) when line endings are ignored the trailing endline is of course also ignored. - if (outText.endsWithNewline) - { - if (!refText.endsWithNewline) - { - writeln(outFileName, " ends with a newline, but ", refFileName, " does not."); - return 1; - } - } - else - { - if (refText.endsWithNewline) - { - writeln(refFileName, " ends with a newline, but ", outFileName, " does not."); - return 1; - } - } } foreach (entry; dirEntries("expected_failures", "*.d", SpanMode.shallow)) @@ -88,16 +65,3 @@ int main() writeln("All tests succeeded."); return 0; } - -bool endsWithNewline(string text) pure -{ - // Same criteria as https://dlang.org/phobos/std_string.html#.lineSplitter - return - text.endsWith('\n') || - text.endsWith('\r') || - text.endsWith(lineSep) || - text.endsWith(paraSep) || - text.endsWith('\u0085') || - text.endsWith('\v') || - text.endsWith('\f'); -} From e20d808a56721906ba7aac860b4dcf128f3b825d Mon Sep 17 00:00:00 2001 From: skyline131313 <11180237+skyline131313@users.noreply.github.com> Date: Mon, 13 Jun 2022 10:23:56 -0400 Subject: [PATCH 2/4] Update dub.json to remove dependency on `rdmd` (#547) --- dub.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dub.json b/dub.json index edaa129b..2086e9e6 100644 --- a/dub.json +++ b/dub.json @@ -14,7 +14,7 @@ "versions" : [ "built_with_dub" ], - "preGenerateCommands" : [ - "rdmd \"$PACKAGE_DIR/dubhash.d\"" + "preBuildCommands" : [ + "$DC -run \"$PACKAGE_DIR/dubhash.d\"" ] } From 4d264ac551ce7656138209b6b120e78c787e9e83 Mon Sep 17 00:00:00 2001 From: belka-ew Date: Tue, 14 Jun 2022 14:35:54 +0200 Subject: [PATCH 3/4] Fix array literal indentation in foreach (#554) --- src/dfmt/formatter.d | 32 ++++++++++++++----- tests/allman/foreach_array.d.ref | 54 ++++++++++++++++++++++++++++++++ tests/foreach_array.args | 1 + tests/foreach_array.d | 53 +++++++++++++++++++++++++++++++ tests/knr/foreach_array.d.ref | 47 +++++++++++++++++++++++++++ tests/otbs/foreach_array.d.ref | 46 +++++++++++++++++++++++++++ tests/test.sh | 2 ++ 7 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 tests/allman/foreach_array.d.ref create mode 100644 tests/foreach_array.args create mode 100644 tests/foreach_array.d create mode 100644 tests/knr/foreach_array.d.ref create mode 100644 tests/otbs/foreach_array.d.ref diff --git a/src/dfmt/formatter.d b/src/dfmt/formatter.d index c64ee9b7..d302f4a4 100644 --- a/src/dfmt/formatter.d +++ b/src/dfmt/formatter.d @@ -15,7 +15,7 @@ import dfmt.indentation; import dfmt.tokens; import dfmt.wrapping; import std.array; -import std.algorithm.comparison : among; +import std.algorithm.comparison : among, max; /** * Formats the code contained in `buffer` into `output`. @@ -213,6 +213,11 @@ private: /// True if the next "else" should be formatted as a single line bool inlineElse; + /// Tracks paren depth on a single line. This information can be used to + /// indent array literals inside parens, since arrays are indented only once + /// and paren indentation is ignored.line breaks and "[" reset the counter. + int parenDepthOnLine; + void formatStep() { import std.range : assumeSorted; @@ -614,6 +619,7 @@ private: writeToken(); if (p == tok!"(") { + ++parenDepthOnLine; // If the file starts with an open paren, just give up. This isn't // valid D code. if (index < 2) @@ -633,9 +639,7 @@ private: if (arrayInitializerStart && isMultilineAt(index - 1)) { - if (peekBack2Is(tok!"(")) { - indents.pop(); - } + revertParenIndentation(); // Use the close bracket as the indent token to distinguish // the array initialiazer from an array index in the newline @@ -659,9 +663,7 @@ private: } else if (p == tok!"[" && config.dfmt_keep_line_breaks == OptionalBoolean.t) { - if (peekBack2Is(tok!"(")) { - indents.pop(); - } + revertParenIndentation(); IndentStack.Details detail; detail.wrap = false; @@ -714,6 +716,19 @@ private: } } + void revertParenIndentation() + { + if (parenDepthOnLine) + { + foreach (i; 0 .. parenDepthOnLine) + { + indents.pop(); + } + indents.popTempIndents(); + } + parenDepthOnLine = 0; + } + void formatRightParen() in { @@ -721,6 +736,7 @@ private: } do { + parenDepthOnLine = max(parenDepthOnLine - 1, 0); parenDepth--; indents.popWrapIndents(); while (indents.topIsOneOf(tok!"!", tok!")")) @@ -1682,6 +1698,8 @@ private: import std.algorithm : max, canFind; import dfmt.editorconfig : OptionalBoolean; + parenDepthOnLine = 0; + if (currentIs(tok!"comment") && index > 0 && current.line == tokenEndLine(tokens[index - 1])) return; diff --git a/tests/allman/foreach_array.d.ref b/tests/allman/foreach_array.d.ref new file mode 100644 index 00000000..e7141295 --- /dev/null +++ b/tests/allman/foreach_array.d.ref @@ -0,0 +1,54 @@ +static foreach (x; [ + 1, + 2, + 3, +]) +{ +} + +static foreach_reverse (x; [ + 1, + 2, + 3, +]) +{ +} + +void f() +{ + foreach (x; [ + 1, + 2, + 3, + ]) + { + } + foreach_reverse (x; [ + 1, + 2, + 3, + ]) + { + } + + if (!SymbolTool.instance.workspacesFilesUris.canFind!sameFile(uri)) + { + send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(uri, [ + ])); + } + + foreach (x; map([ + 1, + 2, + 3, + ])) + { + } + foreach (x; foo!(map!([ + 1, + 2, + 3, + ]))) + { + } +} diff --git a/tests/foreach_array.args b/tests/foreach_array.args new file mode 100644 index 00000000..3e94d38f --- /dev/null +++ b/tests/foreach_array.args @@ -0,0 +1 @@ +--keep_line_breaks true diff --git a/tests/foreach_array.d b/tests/foreach_array.d new file mode 100644 index 00000000..8e8ea0f5 --- /dev/null +++ b/tests/foreach_array.d @@ -0,0 +1,53 @@ +static foreach (x; [ + 1, + 2, + 3, +]) +{ +} + +static foreach_reverse (x; [ + 1, + 2, + 3, +]) +{ +} + +void f() +{ + foreach (x; [ + 1, + 2, + 3, + ]) + { + } + foreach_reverse (x; [ + 1, + 2, + 3, + ]) + { + } + + if (!SymbolTool.instance.workspacesFilesUris.canFind!sameFile(uri)) + { + send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(uri, [])); + } + + foreach (x; map([ + 1, + 2, + 3, + ])) + { + } + foreach (x; foo!(map!([ + 1, + 2, + 3, + ]))) + { + } +} diff --git a/tests/knr/foreach_array.d.ref b/tests/knr/foreach_array.d.ref new file mode 100644 index 00000000..f0d447ed --- /dev/null +++ b/tests/knr/foreach_array.d.ref @@ -0,0 +1,47 @@ +static foreach (x; [ + 1, + 2, + 3, +]) { +} + +static foreach_reverse (x; [ + 1, + 2, + 3, +]) { +} + +void f() +{ + foreach (x; [ + 1, + 2, + 3, + ]) { + } + foreach_reverse (x; [ + 1, + 2, + 3, + ]) { + } + + if (!SymbolTool.instance.workspacesFilesUris.canFind!sameFile(uri)) { + send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(uri, [ + ])); + } + + foreach (x; map([ + 1, + 2, + 3, + ])) { + } + foreach (x; foo!(map!([ + 1, + 2, + 3, + ]))) { + } +} diff --git a/tests/otbs/foreach_array.d.ref b/tests/otbs/foreach_array.d.ref new file mode 100644 index 00000000..b505616f --- /dev/null +++ b/tests/otbs/foreach_array.d.ref @@ -0,0 +1,46 @@ +static foreach (x; [ + 1, + 2, + 3, +]) { +} + +static foreach_reverse (x; [ + 1, + 2, + 3, +]) { +} + +void f() { + foreach (x; [ + 1, + 2, + 3, + ]) { + } + foreach_reverse (x; [ + 1, + 2, + 3, + ]) { + } + + if (!SymbolTool.instance.workspacesFilesUris.canFind!sameFile(uri)) { + send(TextDocument.publishDiagnostics, new PublishDiagnosticsParams(uri, [ + ])); + } + + foreach (x; map([ + 1, + 2, + 3, + ])) { + } + foreach (x; foo!(map!([ + 1, + 2, + 3, + ]))) { + } +} diff --git a/tests/test.sh b/tests/test.sh index 88df2278..7b812772 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -5,6 +5,8 @@ for braceStyle in allman otbs knr do for source in *.d do + test "$(basename $source '.d')" = 'test' && continue + echo "${source}.ref" "${braceStyle}/${source}.out" argsFile=$(basename "${source}" .d).args if [ -e "${argsFile}" ]; then From b172b36b3dafe77d244682ea1d2411b52223c366 Mon Sep 17 00:00:00 2001 From: Bastiaan Veelo Date: Mon, 11 Jul 2022 23:41:08 +0200 Subject: [PATCH 4/4] Move application logic out of the constructor. --- src/dfmt/formatter.d | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/dfmt/formatter.d b/src/dfmt/formatter.d index d302f4a4..8dcab2a2 100644 --- a/src/dfmt/formatter.d +++ b/src/dfmt/formatter.d @@ -105,8 +105,6 @@ struct TokenFormatter(OutputRange) this(const ubyte[] rawSource, const(Token)[] tokens, immutable short[] depths, OutputRange output, ASTInformation* astInformation, Config* config) { - import std.algorithm.searching : countUntil; - this.rawSource = rawSource; this.tokens = tokens; this.depths = depths; @@ -127,18 +125,8 @@ struct TokenFormatter(OutputRange) assert(false, "config.end_of_line was unspecified"); else { - assert (eol == eol._default); // Same as input. - // Intentional wraparound, -1 turns into uint.max when not found: - const firstCR = cast(uint) rawSource.countUntil("\r"); - if (firstCR < cast(uint) rawSource.countUntil("\n")) - { - if (firstCR == rawSource.countUntil("\r\n")) - this.eolString = "\r\n"; - else - this.eolString = "\r"; - } - else - this.eolString = "\n"; + assert (eol == eol._default); + this.eolString = eolStringFromInput; } } } @@ -218,6 +206,17 @@ private: /// and paren indentation is ignored.line breaks and "[" reset the counter. int parenDepthOnLine; + string eolStringFromInput() const + { + import std.algorithm : countUntil; + + // Intentional wraparound, -1 turns into uint.max when not found: + const firstCR = cast(uint) rawSource.countUntil("\r"); + if (firstCR < cast(uint) rawSource.countUntil("\n")) + return firstCR == rawSource.countUntil("\r\n") ? "\r\n" : "\r"; + return "\n"; + } + void formatStep() { import std.range : assumeSorted;