diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java index 83a2c9579..dce148572 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java +++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInputAstVisitor.java @@ -75,7 +75,6 @@ import com.google.googlejavaformat.FormattingError; import com.google.googlejavaformat.Indent; import com.google.googlejavaformat.Input; -import com.google.googlejavaformat.Newlines; import com.google.googlejavaformat.Op; import com.google.googlejavaformat.OpenOp; import com.google.googlejavaformat.OpsBuilder.BlankLineWanted; @@ -1756,21 +1755,8 @@ public Void visitMemberSelect(MemberSelectTree node, Void unused) { public Void visitLiteral(LiteralTree node, Void unused) { sync(node); String sourceForNode = getSourceForNode(node, getCurrentPath()); - if (sourceForNode.startsWith("\"\"\"")) { - String separator = Newlines.guessLineSeparator(sourceForNode); - ImmutableList initialLines = sourceForNode.lines().collect(toImmutableList()); - String stripped = initialLines.stream().skip(1).collect(joining(separator)).stripIndent(); - // Use the last line of the text block to determine if it is deindented to column 0, by - // comparing the length of the line in the input source with the length after processing - // the text block contents with stripIndent(). - boolean deindent = - getLast(initialLines).stripTrailing().length() - == Streams.findLast(stripped.lines()).orElseThrow().stripTrailing().length(); - if (deindent) { - Indent indent = Indent.Const.make(Integer.MIN_VALUE / indentMultiplier, indentMultiplier); - builder.breakOp(indent); - } - token(sourceForNode); + if (sourceForNode.startsWith(TEXT_BLOCK_DELIMITER)) { + visitTextBlock(sourceForNode); return null; } if (isUnaryMinusLiteral(sourceForNode)) { @@ -1781,6 +1767,39 @@ public Void visitLiteral(LiteralTree node, Void unused) { return null; } + private static final String TEXT_BLOCK_DELIMITER = "\"\"\""; + + private void visitTextBlock(String sourceForNode) { + // Find the source code of the text block with incidental whitespace removed. + // The first line of the text block is always """, and it does not affect incidental + // whitespace. + ImmutableList initialLines = sourceForNode.lines().collect(toImmutableList()); + String stripped = initialLines.stream().skip(1).collect(joining("\n")).stripIndent(); + ImmutableList strippedLines = stripped.lines().collect(toImmutableList()); + String lastLine = getLast(strippedLines); + // Use the last line of the text block to determine if it is deindented to column 0, by + // comparing the length of the line in the input source with the length after processing + // the text block contents with stripIndent(). + boolean deindent = getLast(initialLines).stripTrailing().length() == lastLine.length(); + if (deindent) { + Indent indent = Indent.Const.make(Integer.MIN_VALUE / indentMultiplier, indentMultiplier); + builder.breakOp(indent); + } + ImmutableList.Builder lines = ImmutableList.builder(); + lines.add(initialLines.get(0).stripTrailing()); + lines.addAll(strippedLines.subList(0, strippedLines.size() - 1)); + String withoutDelimiter = + lastLine.substring(0, lastLine.length() - TEXT_BLOCK_DELIMITER.length()).stripTrailing(); + if (!withoutDelimiter.isEmpty()) { + lines.add(withoutDelimiter + '\\'); + } + // If the trailing line is just """, indenting it more than the prefix of incidental + // whitespace has no effect, and results in a javac text-blocks warning that 'trailing + // white space will be removed'. + lines.add(TEXT_BLOCK_DELIMITER); + builder.multilineToken(sourceForNode, lines.build()); + } + // A negative numeric literal -n is usually represented as unary minus on n, // but that doesn't work for integer or long MIN_VALUE. The parser works // around that by representing it directly as a signed literal (with no diff --git a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java index 074dcea30..c199b8673 100644 --- a/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java +++ b/core/src/main/java/com/google/googlejavaformat/java/StringWrapper.java @@ -14,11 +14,9 @@ package com.google.googlejavaformat.java; -import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.Iterables.getLast; import static com.google.googlejavaformat.java.Trees.getEndPosition; import static com.google.googlejavaformat.java.Trees.getStartPosition; -import static java.lang.Math.min; import static java.util.stream.Collectors.joining; import com.google.common.base.CharMatcher; @@ -130,24 +128,18 @@ private int visualColumn(int position) { TreeRangeMap getReflowReplacements() { // Paths to string literals that extend past the column limit. List longStringLiterals = new ArrayList<>(); - // Paths to text blocks to be re-indented. - List textBlocks = new ArrayList<>(); - new LongStringsAndTextBlockScanner(longStringLiterals, textBlocks) - .scan(new TreePath(unit), null); + new LongStringsScanner(longStringLiterals).scan(new TreePath(unit), null); TreeRangeMap replacements = TreeRangeMap.create(); - indentTextBlocks(replacements, textBlocks); wrapLongStrings(replacements, longStringLiterals); return replacements; } - private class LongStringsAndTextBlockScanner extends TreePathScanner { + private class LongStringsScanner extends TreePathScanner { private final List longStringLiterals; - private final List textBlocks; - LongStringsAndTextBlockScanner(List longStringLiterals, List textBlocks) { + LongStringsScanner(List longStringLiterals) { this.longStringLiterals = longStringLiterals; - this.textBlocks = textBlocks; } @Override @@ -155,9 +147,7 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) { if (literalTree.getKind() != Kind.STRING_LITERAL) { return null; } - int pos = getStartPosition(literalTree); - if (input.substring(pos, min(input.length(), pos + 3)).equals(TEXT_BLOCK_DELIMITER)) { - textBlocks.add(literalTree); + if (input.startsWith(TEXT_BLOCK_DELIMITER, getStartPosition(literalTree))) { return null; } Tree parent = getCurrentPath().getParentPath().getLeaf(); @@ -178,56 +168,6 @@ public Void visitLiteral(LiteralTree literalTree, Void aVoid) { } } - private void indentTextBlocks( - TreeRangeMap replacements, List textBlocks) { - for (Tree tree : textBlocks) { - int startPosition = lineMap.getStartPosition(lineMap.getLineNumber(getStartPosition(tree))); - int endPosition = getEndPosition(tree, unit); - String text = input.substring(startPosition, endPosition); - int leadingWhitespace = CharMatcher.whitespace().negate().indexIn(text); - - // Find the source code of the text block with incidental whitespace removed. - // The first line of the text block is always """, and it does not affect incidental - // whitespace. - ImmutableList initialLines = text.lines().collect(toImmutableList()); - String stripped = initialLines.stream().skip(1).collect(joining(separator)).stripIndent(); - ImmutableList lines = stripped.lines().collect(toImmutableList()); - boolean deindent = - getLast(initialLines).stripTrailing().length() - == getLast(lines).stripTrailing().length(); - - String prefix = - deindent ? "" : options.indentString(visualColumn(startPosition + leadingWhitespace)); - - StringBuilder output = new StringBuilder(prefix).append(initialLines.get(0).stripLeading()); - for (int i = 0; i < lines.size(); i++) { - String line = lines.get(i); - String trimmed = line.stripTrailing(); - output.append(separator); - if (!trimmed.isEmpty()) { - // Don't add incidental leading whitespace to empty lines - output.append(prefix); - } - if (i == lines.size() - 1) { - String withoutDelimiter = - trimmed - .substring(0, trimmed.length() - TEXT_BLOCK_DELIMITER.length()) - .stripTrailing(); - if (!withoutDelimiter.stripLeading().isEmpty()) { - output.append(withoutDelimiter).append('\\').append(separator).append(prefix); - } - // If the trailing line is just """, indenting it more than the prefix of incidental - // whitespace has no effect, and results in a javac text-blocks warning that 'trailing - // white space will be removed'. - output.append(TEXT_BLOCK_DELIMITER); - } else { - output.append(line); - } - } - replacements.put(Range.closedOpen(startPosition, endPosition), output.toString()); - } - } - private void wrapLongStrings( TreeRangeMap replacements, List longStringLiterals) { for (TreePath path : longStringLiterals) { @@ -450,14 +390,13 @@ private static boolean noComments( private static final CharMatcher STRING_CONCAT_DELIMITER = CharMatcher.whitespace().or(CharMatcher.anyOf("\"+")); - /// Returns true if any lines in the given Java source exceed the column limit, or contain a `"""` - /// that could indicate a text block. + /// Returns true if any lines in the given Java source exceed the column limit. private static boolean needWrapping(int columnLimit, String input, JavaFormatterOptions options) { // TODO(cushon): consider adding Newlines.lineIterable? Iterator it = Newlines.lineIterator(input); while (it.hasNext()) { String line = it.next(); - if (options.visualLength(line) > columnLimit || line.contains(TEXT_BLOCK_DELIMITER)) { + if (options.visualLength(line) > columnLimit) { return true; } } diff --git a/core/src/main/java/com/google/googlejavaformat/layout/Doc.java b/core/src/main/java/com/google/googlejavaformat/layout/Doc.java index 7c8308fb1..4b57a7a7f 100644 --- a/core/src/main/java/com/google/googlejavaformat/layout/Doc.java +++ b/core/src/main/java/com/google/googlejavaformat/layout/Doc.java @@ -56,7 +56,8 @@ /// /// A [Choice] offers a preferred alternative layout for a group, used when the group doesn't fit /// flat but the preferred layout's first line does. -public sealed interface Doc permits Doc.Text, Doc.Tok, Doc.Space, Doc.Break, Doc.Group, Doc.Choice { +public sealed interface Doc + permits Doc.Text, Doc.Lines, Doc.Tok, Doc.Space, Doc.Break, Doc.Group, Doc.Choice { /// Width used for documents that cannot be laid out on a single line. int MAX_LINE_WIDTH = 1000; @@ -117,7 +118,7 @@ public Text(String text) { @Override public int width() { - return Newlines.firstBreak(text) >= 0 ? MAX_LINE_WIDTH : text.length(); + return text.length(); } @Override @@ -126,6 +127,20 @@ public void appendFlat(StringBuilder sb) { } } + /// A multi-line input token whose non-empty lines after the first are indented to the column + /// where the token starts, for example text blocks. + record Lines(ImmutableList lines, Range range) implements Doc { + @Override + public int width() { + return MAX_LINE_WIDTH; + } + + @Override + public void appendFlat(StringBuilder sb) { + throw new UnsupportedOperationException(); + } + } + /// Input text that isn't a token (the equivalent of the `Doc.Tok` leaf in /// [com.google.googlejavaformat.Doc]). This is usually a comment. It can also be whitespace or /// newlines around an *imaginary* token ([RealOrImaginary#IMAGINARY]): a token the AST visitor diff --git a/core/src/main/java/com/google/googlejavaformat/layout/LayoutBuilder.java b/core/src/main/java/com/google/googlejavaformat/layout/LayoutBuilder.java index bcc2a4f93..4f90924ca 100644 --- a/core/src/main/java/com/google/googlejavaformat/layout/LayoutBuilder.java +++ b/core/src/main/java/com/google/googlejavaformat/layout/LayoutBuilder.java @@ -119,7 +119,8 @@ public void drain() { token, RealOrImaginary.IMAGINARY, ZERO, - /* breakAndIndentTrailingComment= */ Optional.empty())); + /* breakAndIndentTrailingComment= */ Optional.empty(), + /* linesIndentedToTokenStart= */ Optional.empty())); } } this.inputPosition = endPosition; @@ -257,6 +258,20 @@ public void token( RealOrImaginary realOrImaginary, Indent plusIndentCommentsBefore, Optional breakAndIndentTrailingComment) { + token( + token, + realOrImaginary, + plusIndentCommentsBefore, + breakAndIndentTrailingComment, + Optional.empty()); + } + + private void token( + String token, + RealOrImaginary realOrImaginary, + Indent plusIndentCommentsBefore, + Optional breakAndIndentTrailingComment, + Optional> linesIndentedToTokenStart) { ImmutableList tokens = input.getTokens(); if (peekToken().filter(token::equals).isPresent()) { events.add( @@ -264,7 +279,8 @@ public void token( tokens.get(tokenI++), RealOrImaginary.REAL, plusIndentCommentsBefore, - breakAndIndentTrailingComment)); + breakAndIndentTrailingComment, + linesIndentedToTokenStart)); } else if (realOrImaginary.isReal()) { // Generated a "bad" token, which doesn't exist on the input. Complain, unless (for example) // we're guessing at an optional token, in which case it's dropped. @@ -275,6 +291,12 @@ public void token( } } + /// Emits a real multi-line token whose non-empty lines after the first are indented to the column + /// where the token starts, for example a text block. + public void multilineToken(String token, ImmutableList lines) { + token(token, RealOrImaginary.REAL, ZERO, Optional.empty(), Optional.of(lines)); + } + public void op(String op) { int opN = op.length(); for (int i = 0; i < opN; i++) { @@ -592,9 +614,11 @@ private static Doc buildTree(List events) { } case TokenOp tokenOp -> { Input.Tok tok = tokenOp.token().getTok(); + Range range = Range.singleton(tok.getIndex()).canonical(INTEGERS); appendLevel.add( - new Doc.Text( - tok.getOriginalText(), Range.singleton(tok.getIndex()).canonical(INTEGERS))); + tokenOp.linesIndentedToTokenStart().isPresent() + ? new Doc.Lines(tokenOp.linesIndentedToTokenStart().get(), range) + : new Doc.Text(tok.getOriginalText(), range)); } case TokOp tokOp -> appendLevel.add(new Doc.Tok(tokOp.tok())); case HugPointOp hugPointOp -> appendLevel.add(hugPointOp.hugPoint().marker); @@ -751,7 +775,8 @@ private record TokenOp( Input.Token token, RealOrImaginary realOrImaginary, Indent plusIndentCommentsBefore, - Optional breakAndIndentTrailingComment) + Optional breakAndIndentTrailingComment, + Optional> linesIndentedToTokenStart) implements Event {} private record TokOp(Input.Tok tok) implements Event {} diff --git a/core/src/main/java/com/google/googlejavaformat/layout/LayoutSolver.java b/core/src/main/java/com/google/googlejavaformat/layout/LayoutSolver.java index c381e84cd..1150dad3f 100644 --- a/core/src/main/java/com/google/googlejavaformat/layout/LayoutSolver.java +++ b/core/src/main/java/com/google/googlejavaformat/layout/LayoutSolver.java @@ -51,7 +51,7 @@ public static String format(Doc doc, int maxWidth) { for (Action action : solve(doc, maxWidth, /* commentsHelper= */ null)) { switch (action) { case Append a -> sb.append(a.text()); - case Newline n -> sb.append('\n'); + case Newline n -> sb.repeat("\n", n.count()); case IndentTo i -> sb.repeat(" ", i.spaces()); } } @@ -63,7 +63,7 @@ public static void format(Doc doc, int maxWidth, Output output, CommentsHelper c for (Action action : solve(doc, maxWidth, commentsHelper)) { switch (action) { case Append a -> output.append(a.text(), a.range()); - case Newline n -> output.append("\n", Doc.EMPTY_RANGE); + case Newline n -> output.append("\n".repeat(n.count()), Doc.EMPTY_RANGE); case IndentTo i -> output.indent(i.spaces()); } } @@ -73,7 +73,12 @@ private sealed interface Action {} private record Append(String text, Range range) implements Action {} - private record Newline() implements Action {} + /// Emits `count` line breaks; `count` is more than 1 when a [Doc.Lines] contains blank lines. + private record Newline(int count) implements Action { + Newline() { + this(1); + } + } private record IndentTo(int spaces) implements Action {} @@ -232,13 +237,35 @@ private State leaf(Doc doc, State state) { case Doc.Group g -> group(g, state); case Doc.Choice c -> choice(c, state); case Doc.Text t -> { - int newline = Newlines.firstBreak(t.text()); - if (newline >= 0) { - endLine(state.column() + newline); - } actions.add(new Append(t.text(), t.range())); yield state.withColumn(state.column() + t.width()); } + case Doc.Lines l -> { + ImmutableList lines = l.lines(); + endLine(state.column() + lines.get(0).length()); + if (firstLineEnded) { + yield state; + } + int indent = lineIndent; + actions.add(new Append(lines.get(0), l.range())); + int newlines = 0; + for (String line : lines.subList(1, lines.size())) { + newlines++; + if (line.isEmpty()) { + continue; + } + actions.add(new Newline(newlines)); + newlines = 0; + if (indent > 0) { + actions.add(new IndentTo(indent)); + } + actions.add(new Append(line, l.range())); + } + if (newlines > 0) { + actions.add(new Newline(newlines)); + } + yield state.withColumn(state.column() + l.width()); + } case Doc.Space s -> { actions.add(new Append(" ", Doc.EMPTY_RANGE)); yield state.withColumn(state.column() + 1); diff --git a/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java b/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java index 9dbd59f3b..3cdb330d0 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/FormatterTest.java @@ -680,13 +680,200 @@ public void testI1205() throws Exception { public interface Foo { private static String foo = - \"\"\" + \""" foo\\ - bar \"\"\"; + bar \"""; + } + """; + String expected = + """ + public interface Foo { + + private static String foo = + \""" + foo\\ + bar\\ + \"""; } """; String formatted = new Formatter().formatSource(input); - assertThat(formatted).isEqualTo(input); + assertThat(formatted).isEqualTo(expected); + } + + // Test that whitespace handling on text block lines only removes spaces, not other control + // characters. + @Test + public void textBlockControlCharacter() throws Exception { + // We want an actual control character in the Java source being formatted, not a unicode escape, + // i.e. the escape below doesn't need to be double-escaped. + String input = + """ + package p; + + public class T { + String s = + \""" + \u0007lorem + \u0007 + ipsum + \"""; + } + """; + String actual = new Formatter().formatSource(input); + assertThat(actual).isEqualTo(input); + } + + @Test + public void textBlockTrailingWhitespace() throws Exception { + String input = + """ + public class T { + String s = + \""" + lorem \s + ipsum + \"""; + } + """; + String expected = + """ + public class T { + String s = + \""" + lorem + ipsum + \"""; + } + """; + String actual = new Formatter().formatSource(input); + assertThat(actual).isEqualTo(expected); + } + + // It would be neat if the formatter could remove the trailing whitespace here, but in general + // it preserves unicode escapes from the original text. + @Test + public void textBlockTrailingWhitespaceUnicodeEscape() throws Exception { + // We want a unicode escape in the Java source being formatted, so it needs to be escaped + // in the string literal in this test. + String input = + """ + public class T { + String s = + \""" + lorem\\u0020 + ipsum + \"""; + } + """; + String expected = + """ + public class T { + String s = + \""" + lorem\\u0020 + ipsum + \"""; + } + """; + String actual = new Formatter().formatSource(input); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void textBlockSpaceTabMix() throws Exception { + String input = + """ + public class T { + String s = + \""" + lorem + \tipsum + \"""; + } + """; + String expected = + """ + public class T { + String s = + \""" + lorem + ipsum + \"""; + } + """; + String actual = new Formatter().formatSource(input); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void textBlockLeadingBlankLine() throws Exception { + String input = + """ + public class T { + String s = + \""" + + lorem + ipsum + \"""; + } + """; + String expected = + """ + public class T { + String s = + \""" + + lorem + ipsum + \"""; + } + """; + String actual = new Formatter().formatSource(input); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void textBlockInHuggedLambda() throws Exception { + String input = + """ + class T { + void f() { + foo( + tree, + "The following matches unexpectedly occurred in method `%s`".formatted(tree.getName()), + unexpectedMatchesByLineNumber.entries().stream() + .map( + e -> + \""" + Rule `%s` matches on line %s, while it should match in a method named \\ + `test%s`.\""" + .formatted(e.getValue(), e.getKey(), e.getValue())) + .collect(toImmutableSet()), + state); + } + } + """; + String expected = + """ + class T { + void f() { + foo( + tree, + "The following matches unexpectedly occurred in method `%s`".formatted(tree.getName()), + unexpectedMatchesByLineNumber.entries().stream() + .map(e -> + \""" + Rule `%s` matches on line %s, while it should match in a method named \\ + `test%s`.\\ + \""" + .formatted(e.getValue(), e.getKey(), e.getValue())) + .collect(toImmutableSet()), + state); + } + } + """; + assertThat(new Formatter().formatSource(input)).isEqualTo(expected); } @Test diff --git a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java index abf719c04..0c69ea61c 100644 --- a/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java +++ b/core/src/test/java/com/google/googlejavaformat/java/StringWrapperTest.java @@ -73,144 +73,6 @@ private ReproBug() { assertThat(StringWrapper.wrap(100, input, new Formatter())).isEqualTo(input); } - // Test that whitespace handling on text block lines only removes spaces, not other control - // characters. - @Test - public void textBlockControlCharacter() throws Exception { - assumeTrue(Runtime.version().feature() >= 15); - // We want an actual control character in the Java source being formatted, not a unicode escape, - // i.e. the escape below doesn't need to be double-escaped. - String input = - """ - package p; - public class T { - String s = - \""" - \u0007lorem - \u0007 - ipsum - \"""; - } - """; - String actual = StringWrapper.wrap(100, input, new Formatter()); - assertThat(actual).isEqualTo(input); - } - - @Test - public void textBlockTrailingWhitespace() throws Exception { - assumeTrue(Runtime.version().feature() >= 15); - @SuppressWarnings("MisleadingEscapedSpace") // TODO(b/496180372): remove - String input = - """ - public class T { - String s = - \""" - lorem \s - ipsum - \"""; - } - """; - String expected = - """ - public class T { - String s = - \""" - lorem - ipsum - \"""; - } - """; - String actual = StringWrapper.wrap(100, input, new Formatter()); - assertThat(actual).isEqualTo(expected); - } - - // It would be neat if the formatter could remove the trailing whitespace here, but in general - // it preserves unicode escapes from the original text. - @Test - public void textBlockTrailingWhitespaceUnicodeEscape() throws Exception { - assumeTrue(Runtime.version().feature() >= 15); - // We want a unicode escape in the Java source being formatted, so it needs to be escaped - // in the string literal in this test. - String input = - """ - public class T { - String s = - \""" - lorem\\u0020 - ipsum - \"""; - } - """; - String expected = - """ - public class T { - String s = - \""" - lorem\\u0020 - ipsum - \"""; - } - """; - String actual = StringWrapper.wrap(100, input, new Formatter()); - assertThat(actual).isEqualTo(expected); - } - - @Test - public void textBlockSpaceTabMix() throws Exception { - assumeTrue(Runtime.version().feature() >= 15); - String input = - """ - public class T { - String s = - \""" - lorem - \tipsum - \"""; - } - """; - String expected = - """ - public class T { - String s = - \""" - lorem - ipsum - \"""; - } - """; - String actual = StringWrapper.wrap(100, input, new Formatter()); - assertThat(actual).isEqualTo(expected); - } - - @Test - public void leadingBlankLine() throws Exception { - assumeTrue(Runtime.version().feature() >= 15); - String input = - """ - public class T { - String s = - \""" - - lorem - ipsum - \"""; - } - """; - String expected = - """ - public class T { - String s = - \""" - - lorem - ipsum - \"""; - } - """; - String actual = StringWrapper.wrap(100, input, new Formatter()); - assertThat(actual).isEqualTo(expected); - } - @Test public void wrapWithMaxLineLengthAndTabs() throws Exception { String input = diff --git a/core/src/test/java/com/google/googlejavaformat/layout/DocTest.java b/core/src/test/java/com/google/googlejavaformat/layout/DocTest.java index daf042b54..13d43f991 100644 --- a/core/src/test/java/com/google/googlejavaformat/layout/DocTest.java +++ b/core/src/test/java/com/google/googlejavaformat/layout/DocTest.java @@ -21,6 +21,7 @@ import static com.google.googlejavaformat.layout.Docs.space; import static com.google.googlejavaformat.layout.Docs.text; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; import com.google.googlejavaformat.Input; import org.junit.Test; @@ -93,10 +94,14 @@ private static Doc.Tok tok(String text) { return new Doc.Tok(new FakeTok(0, text)); } + private static Doc lines(String... lines) { + return new Doc.Lines(ImmutableList.copyOf(lines), Doc.EMPTY_RANGE); + } + @Test - public void textWidth() { + public void textAndLinesWidth() { assertThat(text("hello").width()).isEqualTo(5); - assertThat(text("a\nb").width()).isEqualTo(Doc.MAX_LINE_WIDTH); + assertThat(lines("a", "b").width()).isEqualTo(Doc.MAX_LINE_WIDTH); } @Test diff --git a/core/src/test/java/com/google/googlejavaformat/layout/LayoutSolverTest.java b/core/src/test/java/com/google/googlejavaformat/layout/LayoutSolverTest.java index 96115a9cc..04030e5ee 100644 --- a/core/src/test/java/com/google/googlejavaformat/layout/LayoutSolverTest.java +++ b/core/src/test/java/com/google/googlejavaformat/layout/LayoutSolverTest.java @@ -25,6 +25,7 @@ import static com.google.googlejavaformat.layout.Docs.text; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableList; import com.google.googlejavaformat.Output.BreakTag; import org.junit.Test; import org.junit.runner.RunWith; @@ -254,4 +255,29 @@ public void nestedChoicesAreLinear() { } assertThat(LayoutSolver.format(doc, 999)).isEqualTo("a".repeat(depth) + "{\n}"); } + + @Test + public void linesIndentedToLineIndent() { + Doc doc = + group( + 4, + text("String s ="), + line(), + lines("\"\"\"", "hello", "", "world", "\"\"\""), + text(";")); + assertThat(LayoutSolver.format(doc, 80)) + .isEqualTo( + """ + String s = + \""" + hello + + world + \""";\ + """); + } + + private static Doc lines(String... lines) { + return new Doc.Lines(ImmutableList.copyOf(lines), Doc.EMPTY_RANGE); + } }