[Java. Code Formatting] Extract leading and trailing empty lines before reformatting the javadoc

IDEA-361836

GitOrigin-RevId: 707bb0081905309266aec9cc74802fe6e6092063
This commit is contained in:
Georgii Ustinov
2025-03-19 14:57:28 +03:00
committed by intellij-monorepo-bot
parent 7bd9f15a53
commit 91d37684a1
10 changed files with 219 additions and 68 deletions

View File

@@ -1208,6 +1208,7 @@ public final class JavaChangeSignatureUsageProcessor implements ChangeSignatureU
if (changeInfo.isReturnTypeChanged() && methodDocComment != null) {
CanonicalTypes.Type type = changeInfo.getNewReturnType();
PsiDocTag aReturn = methodDocComment.findTagByName("return");
PsiDocComment oldMethodDocComment = (PsiDocComment)methodDocComment.copy();
if (PsiTypes.voidType().equalsToText(type.getTypeText())) {
if (aReturn != null) {
aReturn.delete();
@@ -1219,7 +1220,7 @@ public final class JavaChangeSignatureUsageProcessor implements ChangeSignatureU
methodDocComment.add(JavaPsiFacade.getElementFactory(method.getProject()).createDocTagFromText("@return"));
}
}
CommonJavaRefactoringUtil.formatJavadocIgnoringSettings(method, methodDocComment);
CommonJavaRefactoringUtil.formatJavadocIgnoringSettings(method, methodDocComment, oldMethodDocComment);
}
}

View File

@@ -30,8 +30,12 @@ public class CommentFormatter {
private final Project myProject;
public CommentFormatter(@NotNull PsiFile file) {
this(file, null);
}
public CommentFormatter(@NotNull PsiFile file, @Nullable PsiDocComment oldComment) {
mySettings = CodeStyle.getSettings(file);
myParser = new JDParser(mySettings);
myParser = new JDParser(mySettings, oldComment);
myProject = file.getProject();
}

View File

@@ -40,6 +40,7 @@ public class JDParser {
private final JavaCodeStyleSettings mySettings;
private final CommonCodeStyleSettings myCommonSettings;
private final @Nullable JDPreformattingContext myPreformattingContext;
private static final String SNIPPET_START_REGEXP = "\\{s*@snippet[^\\}]*";
private static final String PRE_TAG_START_REGEXP = "<pre\\s*(\\w+\\s*=.*)?>";
@@ -68,8 +69,33 @@ public class JDParser {
private static final String[] TAGS_TO_KEEP_INDENTS_AFTER = {"table", "ol", "ul", "div", "dl"};
public JDParser(@NotNull CodeStyleSettings settings) {
this(settings, null);
}
public JDParser(@NotNull CodeStyleSettings settings, @Nullable PsiDocComment oldComment) {
mySettings = settings.getCustomSettings(JavaCodeStyleSettings.class);
myCommonSettings = settings.getCommonSettings(JavaLanguage.INSTANCE);
myPreformattingContext = getPreformattingContext(oldComment);
}
public @Nullable JDPreformattingContext getPreformattingContext(@Nullable PsiElement element) {
if (!(element instanceof PsiDocComment comment)) return null;
DocTextInfo docTextInfo = getDocTextInfo(comment);
boolean isMarkdown = comment.isMarkdownComment();
if (!isMarkdown && !JAVADOC_HEADER.equals(docTextInfo.commentHeader)) return null;
List<Boolean> markers = new ArrayList<>();
List<String> l = toArray(docTextInfo.comment, markers, isMarkdown);
if (l == null) return null;
preprocessLines(l, markers, isMarkdown);
EmptyLinesInfo emptyLinesInfo = getEmptyLinesInfo(l, isMarkdown);
if (emptyLinesInfo == null) return null;
return new JDPreformattingContext(emptyLinesInfo.prefixLinesCount, emptyLinesInfo.suffixLinesCount);
}
public void formatCommentText(@NotNull PsiElement element, @NotNull CommentFormatter formatter) {
@@ -83,7 +109,7 @@ public class JDParser {
}
private static boolean isJavadoc(CommentInfo info) {
return info.docComment.isMarkdownComment() || JAVADOC_HEADER.equals(info.commentHeader);
return info.docComment.isMarkdownComment() || JAVADOC_HEADER.equals(info.docTextInfo.commentHeader);
}
private static CommentInfo getElementsCommentInfo(@Nullable PsiElement psiElement) {
@@ -110,6 +136,11 @@ public class JDParser {
}
private static CommentInfo getCommentInfo(@NotNull PsiDocComment docComment, @NotNull PsiElement owner) {
DocTextInfo docTextInfo = getDocTextInfo(docComment);
return new CommentInfo(docComment, owner, docTextInfo);
}
private static @NotNull JDParser.DocTextInfo getDocTextInfo(@NotNull PsiDocComment docComment) {
String commentHeader = null;
String commentFooter = null;
@@ -134,17 +165,17 @@ public class JDParser {
sb.append(text);
}
return new CommentInfo(docComment, owner, commentHeader, sb.toString(), commentFooter);
return new DocTextInfo(commentHeader, sb.toString(), commentFooter);
}
private @NotNull JDComment parse(@NotNull CommentInfo info, @NotNull CommentFormatter formatter) {
JDComment comment = createComment(info.commentOwner, formatter, info.docComment.isMarkdownComment());
parse(info.comment, comment);
if (info.commentHeader != null) {
comment.setFirstCommentLine(info.commentHeader);
parse(info.docTextInfo.comment, comment);
if (info.docTextInfo.commentHeader != null) {
comment.setFirstCommentLine(info.docTextInfo.commentHeader);
}
if (info.commentFooter != null) {
comment.setLastCommentLine(info.commentFooter);
if (info.docTextInfo.commentFooter != null) {
comment.setLastCommentLine(info.docTextInfo.commentFooter);
}
return comment;
}
@@ -180,60 +211,17 @@ public class JDParser {
int size = l.size();
if (size == 0) return;
// preprocess strings - removes leading token
for (int i = 0; i < size; i++) {
String line = l.get(i);
line = line.trim();
if (!line.isEmpty()) {
if(!comment.getIsMarkdown()){
if (line.charAt(0) == '*') {
if ((markers.get(i)).booleanValue()) {
if (line.length() > 1 && line.charAt(1) == ' ') {
line = line.substring(2);
}
else {
line = line.substring(1);
}
}
else {
line = line.substring(1).trim();
}
}
} else {
// Note: Markdown comments are not trimmed like html ones, except for javadoc tags
String newLine;
int tagStart = CharArrayUtil.shiftForward(line, 3, " \t");
if (tagStart != line.length() && line.charAt(tagStart) == '@') {
newLine = line.substring(tagStart);
} else {
newLine = StringUtil.trimStart(line, "/// ");
if (Strings.areSameInstance(newLine, line)) {
newLine = StringUtil.trimStart(line, "///");
}
}
line = newLine;
}
}
l.set(i, line);
preprocessLines(l, markers, comment.getIsMarkdown());
if (myPreformattingContext != null) {
comment.setPrefixEmptyLineCount(myPreformattingContext.getPrefixLinesCount());
comment.setSuffixEmptyLineCount(myPreformattingContext.getSuffixLinesCount());
}
if (mySettings.shouldKeepEmptyTrailingLines()) {
// counting for the empty lines in the prefix and the suffix of the javadoc to restore them in the future
int prefixLineCount = 0;
int suffixLineCount = 0;
while (prefixLineCount < size && l.get(prefixLineCount).isEmpty()) prefixLineCount++;
if (prefixLineCount == size) {
if (comment.getIsMarkdown()) prefixLineCount--;
comment.setPrefixEmptyLineCount(prefixLineCount);
comment.setSuffixEmptyLineCount(0);
}
else {
while (suffixLineCount < size && l.get(size - suffixLineCount - 1).isEmpty()) suffixLineCount++;
comment.setPrefixEmptyLineCount(prefixLineCount);
comment.setSuffixEmptyLineCount(suffixLineCount);
else {
EmptyLinesInfo emptyLinesInfo = getEmptyLinesInfo(l, comment.getIsMarkdown());
if (emptyLinesInfo != null) {
comment.setPrefixEmptyLineCount(emptyLinesInfo.prefixLinesCount);
comment.setSuffixEmptyLineCount(emptyLinesInfo.suffixLinesCount);
}
}
@@ -295,6 +283,64 @@ public class JDParser {
}
}
private @Nullable EmptyLinesInfo getEmptyLinesInfo(@NotNull List<String> l, boolean isMarkdown) {
if (!mySettings.shouldKeepEmptyTrailingLines()) return null;
// counting for the empty lines in the prefix and the suffix of the javadoc to restore them in the future
int prefixLineCount = 0;
int suffixLineCount = 0;
int size = l.size();
while (prefixLineCount < size && l.get(prefixLineCount).isEmpty()) prefixLineCount++;
if (prefixLineCount == size) {
if (isMarkdown) prefixLineCount--;
return new EmptyLinesInfo(prefixLineCount, 0);
}
else {
while (suffixLineCount < size && l.get(size - suffixLineCount - 1).isEmpty()) suffixLineCount++;
return new EmptyLinesInfo(prefixLineCount, suffixLineCount);
}
}
private static void preprocessLines(List<String> l, List<Boolean> markers, boolean isMarkdown) {
// preprocess strings - removes leading token
for (int i = 0; i < l.size(); i++) {
String line = l.get(i);
line = line.trim();
if (!line.isEmpty()) {
if(!isMarkdown){
if (line.charAt(0) == '*') {
if ((markers.get(i)).booleanValue()) {
if (line.length() > 1 && line.charAt(1) == ' ') {
line = line.substring(2);
}
else {
line = line.substring(1);
}
}
else {
line = line.substring(1).trim();
}
}
} else {
// Note: Markdown comments are not trimmed like html ones, except for javadoc tags
String newLine;
int tagStart = CharArrayUtil.shiftForward(line, 3, " \t");
if (tagStart != line.length() && line.charAt(tagStart) == '@') {
newLine = line.substring(tagStart);
} else {
newLine = StringUtil.trimStart(line, "/// ");
if (Strings.areSameInstance(newLine, line)) {
newLine = StringUtil.trimStart(line, "///");
}
}
line = newLine;
}
}
l.set(i, line);
}
}
/**
* Breaks the specified string by the specified separators into array of strings
*
@@ -968,7 +1014,12 @@ public class JDParser {
return false;
}
private record CommentInfo(PsiDocComment docComment, PsiElement commentOwner, String commentHeader, String comment,
String commentFooter) {
private record CommentInfo(PsiDocComment docComment, PsiElement commentOwner, DocTextInfo docTextInfo) {
}
private record DocTextInfo(String commentHeader, String comment, String commentFooter) {
}
private record EmptyLinesInfo(int prefixLinesCount, int suffixLinesCount) {
}
}

View File

@@ -0,0 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl.source.codeStyle.javadoc
class JDPreformattingContext(val prefixLinesCount : Int, val suffixLinesCount : Int)

View File

@@ -716,6 +716,7 @@ public final class CommonJavaRefactoringUtil {
@NotNull Condition<? super Pair<PsiParameter, String>> eqCondition,
@NotNull Condition<? super String> matchedToOldParam) throws IncorrectOperationException {
if (docComment == null) return;
PsiDocComment oldDocComment = (PsiDocComment)docComment.copy();
final PsiParameter[] parameters = method.getParameterList().getParameters();
final PsiDocTag[] paramTags = docComment.findTagsByName("param");
if (parameters.length > 0 && newParameters.size() < parameters.length && paramTags.length == 0) return;
@@ -778,10 +779,14 @@ public final class CommonJavaRefactoringUtil {
? docComment.addAfter(psiDocTag, anchor)
: docComment.add(psiDocTag);
}
formatJavadocIgnoringSettings(method, docComment);
formatJavadocIgnoringSettings(method, docComment, oldDocComment);
}
public static void formatJavadocIgnoringSettings(@NotNull PsiMethod method, @NotNull PsiDocComment docComment) {
formatJavadocIgnoringSettings(method, docComment, null);
}
public static void formatJavadocIgnoringSettings(@NotNull PsiMethod method, @NotNull PsiDocComment docComment, @Nullable PsiDocComment oldComment) {
PsiFile containingFile = method.getContainingFile();
if (containingFile == null) {
return;
@@ -793,11 +798,9 @@ public final class CommonJavaRefactoringUtil {
boolean javadocEnabled = settings.ENABLE_JAVADOC_FORMATTING;
try {
settings.ENABLE_JAVADOC_FORMATTING = true;
settings.JD_KEEP_TRAILING_EMPTY_LINES = false;
CommentFormatter formatter = new CommentFormatter(method.getContainingFile());
CommentFormatter formatter = new CommentFormatter(method.getContainingFile(), oldComment);
formatter.processComment(docComment.getNode());
} finally {
settings.JD_KEEP_TRAILING_EMPTY_LINES = true;
settings.ENABLE_JAVADOC_FORMATTING = javadocEnabled;
}
}

View File

@@ -0,0 +1,20 @@
class A {
/**
*
*
* Demo.
*
* @param a
* a.
* @param b
* b.
* @param c
* c.
*
*
*
*/
public void <caret>demo(int a, int b, int c) {
}
}

View File

@@ -0,0 +1,19 @@
class A {
///
///
/// Demo.
///
/// @param a
/// a.
/// @param b
/// b.
/// @param c
/// c.
///
///
///
public void <caret>demo(int a, int b, int c) {
}
}

View File

@@ -0,0 +1,16 @@
class A {
///
///
/// Demo.
///
/// @param b b.
/// @param a a.
/// @param c c.
///
///
///
public void demo(int b, int a, int c) {
}
}

View File

@@ -0,0 +1,17 @@
class A {
/**
*
*
* Demo.
*
* @param b b.
* @param a a.
* @param c c.
*
*
*
*/
public void demo(int b, int a, int c) {
}
}

View File

@@ -654,6 +654,14 @@ public class ChangeSignatureTest extends ChangeSignatureBaseTest {
}, false);
}
public void testPreserveEmptyTrailingLeadingLinesJavadoc() {
doTest(null, null, null, method -> new ParameterInfoImpl[]{
ParameterInfoImpl.create(1).withType(PsiTypes.intType()).withName("b"),
ParameterInfoImpl.create(0).withType(PsiTypes.intType()).withName("a"),
ParameterInfoImpl.create(2).withType(PsiTypes.intType()).withName("c"),
}, false);
}
public void testMultilineJavadocWithoutFormatting() { // IDEA-281568
JavaCodeStyleSettings.getInstance(getProject()).ENABLE_JAVADOC_FORMATTING = false;
doTest(null, null, null, method -> new ParameterInfoImpl[]{
@@ -902,6 +910,14 @@ public class ChangeSignatureTest extends ChangeSignatureBaseTest {
}, false);
}
public void testPreserveEmptyTrailingLeadingLinesJavadocMarkdown() {
doTest(null, null, null, method -> new ParameterInfoImpl[]{
ParameterInfoImpl.create(1).withType(PsiTypes.intType()).withName("b"),
ParameterInfoImpl.create(0).withType(PsiTypes.intType()).withName("a"),
ParameterInfoImpl.create(2).withType(PsiTypes.intType()).withName("c"),
}, false);
}
public void testNoGapsInParameterTagsMarkdown() { // IDEA-139879
doTest(null, null, null, method -> new ParameterInfoImpl[]{
ParameterInfoImpl.create(0).withType(PsiTypes.intType()).withName("b"),