From 562a88e4d7fd092cd803a5e4b8c3234042b80702 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 13 Aug 2021 19:55:05 -0700 Subject: [PATCH] Add mutli-character newlines support in CodeWriter CodeWriter now support multi-character newlines like '\r\n'. --- .../amazon/smithy/utils/CodeWriter.java | 105 ++++++++++-------- .../amazon/smithy/utils/CodeWriterTest.java | 36 ++++-- 2 files changed, 89 insertions(+), 52 deletions(-) diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java index 028a6edc2af..0426853294d 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java @@ -545,7 +545,7 @@ public String toString() { } if (result.isEmpty()) { - return trailingNewline ? String.valueOf(currentState.newline) : ""; + return trailingNewline ? currentState.newline : ""; } // This accounts for cases where the only write on the CodeWriter was @@ -556,10 +556,10 @@ public String toString() { if (trailingNewline) { // Add a trailing newline if needed. - return result.charAt(result.length() - 1) != currentState.newline ? result + currentState.newline : result; - } else if (result.charAt(result.length() - 1) == currentState.newline) { + return result.endsWith(currentState.newline) ? result : result + currentState.newline; + } else if (result.endsWith(currentState.newline)) { // Strip the trailing newline if present. - return result.substring(0, result.length() - 1); + return result.substring(0, result.length() - currentState.newline.length()); } else { return result; } @@ -663,8 +663,9 @@ public CodeWriter popState() { // and not written to the builder of the parent state. This ensures that // inline sections are captured inside of strings and then later written // back into a parent state. - popped.builder.setLength(0); - popped.builder.append(result); + StringBuilder builder = popped.getBuilder(); + builder.setLength(0); + builder.append(result); } else if (!result.isEmpty()) { // Sections can be added that are just placeholders. In those cases, // do not write anything unless the section emitted a non-empty string. @@ -677,16 +678,12 @@ public CodeWriter popState() { } private String getTrimmedPoppedStateContents(State state) { - StringBuilder builder = state.builder; - String result = ""; + String result = state.toString(); // Remove the trailing newline, if present, since it gets added in the // final call to writeOptional. - if (builder != null && builder.length() > 0) { - if (builder.charAt(builder.length() - 1) == currentState.newline) { - builder.delete(builder.length() - 1, builder.length()); - } - result = builder.toString(); + if (result.endsWith(currentState.newline)) { + result = result.substring(0, result.length() - currentState.newline.length()); } return result; @@ -823,9 +820,8 @@ public CodeWriter enableNewlines() { * {@link #disableNewlines()}, and does not actually change the newline * character of the current state. * - *

When the provided string is not empty, then the string must contain - * exactly one character. Setting the newline character to a non-empty - * string also implicitly enables newlines in the current state. + *

Setting the newline character to a non-empty string implicitly + * enables newlines in the current state. * * @param newline Newline character to use. * @return Returns the CodeWriter. @@ -833,10 +829,9 @@ public CodeWriter enableNewlines() { public final CodeWriter setNewline(String newline) { if (newline.isEmpty()) { return disableNewlines(); - } else if (newline.length() > 1) { - throw new IllegalArgumentException("newline must be set to an empty string or a single character"); } else { - return setNewline(newline.charAt(0)); + currentState.newline = newline; + return enableNewlines(); } } @@ -851,9 +846,7 @@ public final CodeWriter setNewline(String newline) { * @return Returns the CodeWriter. */ public final CodeWriter setNewline(char newline) { - currentState.newline = newline; - enableNewlines(); - return this; + return setNewline(String.valueOf(newline)); } /** @@ -1350,7 +1343,7 @@ private final class State { private int indentation; private boolean trimTrailingSpaces; private boolean disableNewline; - private char newline = '\n'; + private String newline = "\n"; private char expressionStart = '$'; private transient String sectionName; @@ -1422,41 +1415,62 @@ void putInterceptor(String section, Consumer interceptor) { interceptors.computeIfAbsent(section, s -> new ArrayList<>()).add(interceptor); } - void write(String contents) { + StringBuilder getBuilder() { if (builder == null) { builder = new StringBuilder(); } + return builder; + } + + void write(String contents) { + int position = 0; + int nextNewline = contents.indexOf(newline); - // Write each character, accounting for newlines along the way. - for (int i = 0; i < contents.length(); i++) { - append(contents.charAt(i)); + while (nextNewline > -1) { + for (; position < nextNewline; position++) { + append(contents.charAt(position)); + } + writeNewline(); + position += newline.length(); + nextNewline = contents.indexOf(newline, position); } + + // Write anything remaining in the string after the last newline. + for (; position < contents.length(); position++) { + append(contents.charAt(position)); + } + } + + private void append(char c) { + checkIndentationBeforeWriting(); + getBuilder().append(c); } - void append(char c) { + private void checkIndentationBeforeWriting() { if (needsIndentation) { - builder.append(leadingIndentString); - builder.append(newlinePrefix); + getBuilder().append(leadingIndentString).append(newlinePrefix); needsIndentation = false; } + } - if (c == newline) { - // The next appended character will get indentation and a - // leading prefix string. - needsIndentation = true; - // Trim spaces before each newline. This only mutates the builder - // if space trimming is enabled. - trimSpaces(); - } - - builder.append(c); + private void writeNewline() { + checkIndentationBeforeWriting(); + // Trim spaces before each newline. This only mutates the builder + // if space trimming is enabled. + trimSpaces(); + // Newlines are never split across writes, which could potentially cause + // indentation logic to mess it up. + getBuilder().append(newline); + // The next appended character will get indentation and a + // leading prefix string. + needsIndentation = true; } - void writeLine(String line) { + private void writeLine(String line) { write(line); if (!disableNewline) { - append(newline); + writeNewline(); } } @@ -1465,9 +1479,10 @@ private void trimSpaces() { return; } + StringBuilder buffer = getBuilder(); int toRemove = 0; - for (int i = builder.length() - 1; i > 0; i--) { - if (builder.charAt(i) == ' ') { + for (int i = buffer.length() - 1; i > 0; i--) { + if (buffer.charAt(i) == ' ') { toRemove++; } else { break; @@ -1475,7 +1490,7 @@ private void trimSpaces() { } if (toRemove > 0) { - builder.delete(builder.length() - toRemove, builder.length()); + buffer.delete(buffer.length() - toRemove, buffer.length()); } } diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java index 53f42e3511d..c25cd08b77e 100644 --- a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java +++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java @@ -619,13 +619,6 @@ public void writeInlineDoesNotAllowIndentationToBeEscaped() { assertThat(result, equalTo("\t\t{foo: [\n\t\t\thi,\n\t\t\tbye\n\t\t]\n\t}")); } - @Test - public void newlineLengthMustBe1() { - Assertions.assertThrows(IllegalArgumentException.class, () -> { - CodeWriter.createDefault().setNewline(" "); - }); - } - @Test public void newlineCanBeDisabled() { CodeWriter writer = CodeWriter @@ -653,6 +646,34 @@ public void newlineCanBeDisabledWithEmptyString() { assertThat(result, equalTo("[hi]\n")); } + @Test + public void newlineCanBeMultipleCharacters() { + CodeWriter writer = CodeWriter + .createDefault() + .insertTrailingNewline() + .setNewline("\r\n"); + String result = writer + .openBlock("[", "]", () -> writer.write("hi")) + .enableNewlines() + .toString(); + + assertThat(result, equalTo("[\r\n hi\r\n]\r\n")); + } + + @Test + public void newlineCanBeLotsOfCharacters() { + CodeWriter writer = CodeWriter + .createDefault() + .insertTrailingNewline() + .setNewline("HELLO_THIS_IS_A_NEWLINE!!!"); + String result = writer + .write("Hi.") + .write("There.") + .toString(); + + assertThat(result, equalTo("Hi.HELLO_THIS_IS_A_NEWLINE!!!There.HELLO_THIS_IS_A_NEWLINE!!!")); + } + @Test public void settingNewlineEnablesNewlines() { CodeWriter writer = CodeWriter.createDefault(); @@ -726,6 +747,7 @@ public void canComposeSetWithSection() { assertThat(writer.toString(), equalTo("[1, 2, 3]\n")); } + @Test public void sectionWithWrite() { String testSection = "TEST_SECTION"; CodeWriter writer = new CodeWriter();