diff --git a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java index 78116960a..1cef3a8b1 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocLexer.java @@ -55,12 +55,14 @@ import static java.util.regex.Pattern.compile; import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.PeekingIterator; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** Lexer for the Javadoc formatter. */ @@ -74,9 +76,52 @@ static ImmutableList lex(String input) throws LexException { */ input = stripJavadocBeginAndEnd(input); input = normalizeLineEndings(input); + input = stripMargins(input); return new JavadocLexer(new CharStream(input)).generateTokens(); } + /** + * Strips the margin from every continuation line: the {@code *} prefix (with the space after it) from lines that + * have one, and the comment's own indentation from lines that do not. What remains of a bare line's indentation is + * relative to the comment, as on {@code *} lines after their {@code * }, so a {@code
{@code} block written
+     * without the {@code *} margin keeps its shape for {@link #deindentPreCodeBlocks}.
+     *
+     * 

The indentation stripped from bare lines is the smaller of the shortest {@code *} prefix and the shortest + * indentation of a bare line, so a bare line is never cut into and lines of both kinds stay aligned with each + * other. + */ + private static String stripMargins(String input) { + List lines = Splitter.on('\n').splitToList(input); + List continuations = lines.subList(1, lines.size()); + int starPrefixLength = continuations.stream() + .map(STAR_PREFIX_PATTERN::matcher) + .filter(Matcher::find) + .mapToInt(Matcher::end) + .min() + .orElse(0); + int bareIndentation = continuations.stream() + .filter(line -> !STAR_PREFIX_PATTERN.matcher(line).find()) + .filter(NOT_SPACE_OR_TAB::matchesAnyOf) + .mapToInt(NOT_SPACE_OR_TAB::indexIn) + .min() + .orElse(0); + int bareStrip = Math.min(starPrefixLength, bareIndentation); + StringBuilder result = new StringBuilder(lines.get(0)); + for (String line : continuations) { + result.append('\n'); + Matcher star = STAR_PREFIX_PATTERN.matcher(line); + if (star.find()) { + result.append(line, star.end(), line.length()); + } else if (NOT_SPACE_OR_TAB.matchesAnyOf(line)) { + result.append(line, bareStrip, line.length()); + } + } + return result.toString(); + } + + private static final Pattern STAR_PREFIX_PATTERN = compile("^[ \t]*[*][ \t]?"); + private static final CharMatcher NOT_SPACE_OR_TAB = CharMatcher.noneOf(" \t"); + /** The lexer crashes on windows line endings, so for now just normalize to `\n`. */ // TODO(cushon): use the platform line separator for output private static String normalizeLineEndings(String input) { @@ -528,8 +573,11 @@ private static boolean hasMultipleNewlines(String s) { * * We'd remove the trailing whitespace later on (in JavaCommentsHelper.rewrite), but I feel safer * stripping it now: It otherwise might confuse our line-length count, which we use for wrapping. + * + * The margin of the next line (its `*` and the comment's indentation) is gone already: see + * stripMargins(). */ - private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n[ \t]*[*]?[ \t]?"); + private static final Pattern NEWLINE_PATTERN = compile("^[ \t]*\n"); // We ensure elsewhere that we match this only at the beginning of a line. // Only match tags that start with a lowercase letter, to avoid false matches on unescaped diff --git a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java index 884efb9f5..e86855742 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/java/javadoc/JavadocWriter.java @@ -23,11 +23,13 @@ import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE; import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE; import static com.palantir.javaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG; +import static com.palantir.javaformat.java.javadoc.Token.Type.HTML_COMMENT; import static com.palantir.javaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG; import static com.palantir.javaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Ordering; +import java.util.List; import javax.annotation.Nullable; /** @@ -263,7 +265,14 @@ void writeMoeEndStripComment(Token token) { void writeHtmlComment(Token token) { requestNewline(); - writeToken(token); + // The lexer has stripped the margin from every line, so each one gets the margin back here. + List lines = token.getValue().lines().toList(); + writeToken(new Token(HTML_COMMENT, lines.get(0))); + for (String line : lines.subList(1, lines.size())) { + writeNewline(NO_AUTO_INDENT); + output.append(line); + remainingOnLine -= line.length(); + } requestNewline(); } diff --git a/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java b/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java index da51929fe..40bcb4ad5 100644 --- a/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java +++ b/open-java-format/src/test/java/com/palantir/javaformat/java/JavadocFormattingTest.java @@ -109,7 +109,7 @@ void commentMostlyUntouched() { "/**", " * Foo.", " *", " * bar", " */", "class Test {}", }; String[] expected = { - "/**", " * Foo.", " * ", " * bar", " */", "class Test {}", + "/**", " * Foo.", " * ", " * bar", " */", "class Test {}", }; doFormatTest(input, expected); } @@ -1496,4 +1496,104 @@ void snippetKeepsCommentsAndIndentation() { }; doFormatTest(input, input); } + + @Test + void preCodeWithoutLeadingStarPreservesIndent() { + String[] input = { + "/**", // + " * Example:", + " *

{@code",
+            "class Demo {",
+            "  // Comment",
+            "  static final int X = 1;",
+            "",
+            "  public static void example() {",
+            "    int y = 2;",
+            "  }",
+            "}",
+            " * }
", + " */", + "class Test {}", + }; + String[] expected = { + "/**", // + " * Example:", + " *", + " *
{@code",
+            " * class Demo {",
+            " *   // Comment",
+            " *   static final int X = 1;",
+            " *",
+            " *   public static void example() {",
+            " *     int y = 2;",
+            " *   }",
+            " * }",
+            " * }
", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } + + @Test + void preCodeWithoutLeadingStarInIndentedComment() { + String[] input = { + "class Outer {", // + " /**", + " * Usage:", + " *
{@code",
+            "        Demo demo = new Demo();",
+            "        if (demo.ready()) {",
+            "            demo.run();",
+            "        }",
+            "     * }
", + " */", + " void m() {}", + "}", + }; + String[] expected = { + "class Outer {", // + " /**", + " * Usage:", + " *", + " *
{@code",
+            "   * Demo demo = new Demo();",
+            "   * if (demo.ready()) {",
+            "   *     demo.run();",
+            "   * }",
+            "   * }
", + " */", + " void m() {}", + "}", + }; + doFormatTest(input, expected); + } + + @Test + void preCodeMixedStarAndBareLinesKeepRelativeIndent() { + String[] input = { + "/**", // + " * Example:", + " *
{@code",
+            " * class Demo {",
+            "     int x;",
+            " * }",
+            " * }
", + " */", + "class Test {}", + }; + String[] expected = { + "/**", // + " * Example:", + " *", + " *
{@code",
+            " * class Demo {",
+            " *   int x;",
+            " * }",
+            " * }
", + " */", + "class Test {}", + }; + doFormatTest(input, expected); + } }