int (String line, int width) { if (line.length() < width) { return line.length(); } int preferredBreakPoint = -1; int backupBreakPoint = -1; int tagBraceBalance = 0; for (int i = 0; i < line.length() && (i <= width || backupBreakPoint < 0); i++) { char c = line.charAt(i); if (tagBraceBalance > 0) { if (c == '{') { tagBraceBalance++; } else if (c == '}') { tagBraceBalance--; } } else if (c == '@' && i > 0 && line.charAt(i - 1) == '{') { // We're now inside of an inline tag. Start keeping track of the balance // of opening and closing braces to determine when we've left the tag. tagBraceBalance++; } else if (c == ' ') { preferredBreakPoint = i; } // We'll use this position in a pitch if we can't break somewhere not inside a tag. if (c == ' ') { backupBreakPoint = i; } } if (preferredBreakPoint > 0) { return preferredBreakPoint; } if (backupBreakPoint > 0) { return backupBreakPoint; } return line.length(); } /** * Processes given string and produces on its basis set of pairs like {@code '(string; flag)'} where {@code 'string'} * is interested line and {@code 'flag'} indicates if it is wrapped to {@code <pre>} tag. * * @param s string to process * @return processing result */ @Contract("null -> null") private List<Pair<String, Boolean>> splitToParagraphs(@Nullable String s) { if (s == null) return null; s = s.trim(); if (s.isEmpty()) return null; List<Pair<String, Boolean>> result = new ArrayList<>(); StringBuilder sb = new StringBuilder(); List<Boolean> markers = new ArrayList<>(); List<String> list = toArray(s, markers); Boolean[] marks = markers.toArray(new Boolean[0]); markers.clear(); assert list != null; for (int i = 0; i < list.size(); i++) { String s1 = list.get(i); if (marks[i].booleanValue()) { if (sb.length() != 0) { result.add(new Pair<>(sb.toString(), false)); sb.setLength(0); } result.add(Pair.create(s1, marks[i])); } else { if (s1.isEmpty() || s1.equals(SELF_CLOSED_P_TAG) || isKeepLineFeedsIn(s1)) { endParagraph(result, sb); result.add(Pair.create(s1, marks[i])); } else { if (sb.length() != 0) sb.append(' '); sb.append(s1); } } } if (!mySettings.JD_PRESERVE_LINE_FEEDS && sb.length() != 0) { result.add(new Pair<>(sb.toString(), false)); } return result; } private boolean isKeepLineFeedsIn(@NotNull String line) { return mySettings.JD_PRESERVE_LINE_FEEDS || HtmlUtil.startsWithTag(line); } private static void endParagraph(@NotNull List<? super Pair<String, Boolean>> result, @NotNull StringBuilder sb) { if (sb.length() > 0) { result.add(new Pair<>(sb.toString(), false)); sb.setLength(0); } } private interface TagParser { boolean parse(String tag, String line, JDComment c); } private static final TagParser[] tagParsers = { (tag, line, c) -> { boolean isMyTag = JDTag.SEE.tagEqual(tag); if (isMyTag) { c.addSeeAlso(line); } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = JDTag.SINCE.tagEqual(tag); if (isMyTag) { c.addSince(line); } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = c instanceof JDClassComment && JDTag.VERSION.tagEqual(tag); if (isMyTag) { ((JDClassComment)c).setVersion(line); } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = JDTag.DEPRECATED.tagEqual(tag); if (isMyTag) { c.setDeprecated(line); } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = c instanceof JDMethodComment && JDTag.RETURN.tagEqual(tag); if (isMyTag) { ((JDMethodComment)c).addReturnTag(line); } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = c instanceof JDParamListOwnerComment && JDTag.PARAM.tagEqual(tag); if (isMyTag) { JDParamListOwnerComment mc = (JDParamListOwnerComment)c; int idx; for (idx = 0; idx < line.length(); idx++) { char ch = line.charAt(idx); if (Character.isWhitespace(ch)) break; } if (idx == line.length()) { mc.addParameter(line, ""); } else { String name = line.substring(0, idx); String desc = line.substring(idx).trim(); mc.addParameter(name, desc); } } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = c instanceof JDMethodComment && (JDTag.THROWS.tagEqual(tag) || JDTag.EXCEPTION.tagEqual(tag)); if (isMyTag) { JDMethodComment mc = (JDMethodComment)c; int idx; for (idx = 0; idx < line.length(); idx++) { char ch = line.charAt(idx); if (Character.isWhitespace(ch)) break; } if (idx == line.length()) { mc.addThrow(line, ""); } else { String name = line.substring(0, idx); String desc = line.substring(idx).trim(); mc.addThrow(name, desc); } } return isMyTag; }, (tag, line, c) -> { boolean isMyTag = c instanceof JDClassComment && JDTag.AUTHOR.tagEqual(tag); if (isMyTag) { ((JDClassComment)c).addAuthor(line.trim()); } return isMyTag; } }; private static boolean lineHasUnclosedPreTag(@NotNull String line) { return getOccurenceCount(line, PRE_TAG_START_PATTERN) > StringUtil.getOccurrenceCount(line, PRE_TAG_END); } private static int getLineSnippetTagBraceBalance(@NotNull String line) { int balance = 0; for (int i = 0; i < line.length(); i++) { var ch = line.charAt(i); if (ch == '}') { balance--; } else if (ch == '{') { balance++; } } return balance; } private static boolean lineHasUnclosedSnippetTag(@NotNull String line) { var matcher = SNIPPET_START_PATTERN.matcher(line); var hasResult = false; var lastEnd = -1; do { hasResult = matcher.find(); if (hasResult) { lastEnd = matcher.end(); } } while (hasResult); return lastEnd == line.length(); } private static boolean lineHasClosingPreTag(@NotNull String line) { return StringUtil.getOccurrenceCount(line, PRE_TAG_END) > getOccurenceCount(line, PRE_TAG_START_PATTERN); } @SuppressWarnings("SameParameterValue") private static int getOccurenceCount(@NotNull String line, @NotNull Pattern pattern) { Matcher matcher = pattern.matcher(line); int count = 0; while (matcher.find()) { count++; } return count; } @NotNull protected StringBuilder formatJDTagDescription(@Nullable String str, @NotNull CharSequence prefix) { return formatJDTagDescription(str, prefix, prefix); } /** * Returns formatted JavaDoc tag description, according to selected configuration. Prefixs * may be specified for the first lines and all subsequent lines. This distinction allows * partially manual formatting of the first line (by moving content from the description * to the first line prefix) and allow continuation lines to use different indentation. * * @param str JavaDoc tag description * @param firstLinePrefix prefix to be added to the first line * @param continuationPrefix prefix to be added to lines after the first * @return formatted JavaDoc tag description */ @NotNull protected StringBuilder formatJDTagDescription(@Nullable String str, @NotNull CharSequence firstLinePrefix, @NotNull CharSequence continuationPrefix) { final int rightMargin = myCommonSettings.getRootSettings().getRightMargin(JavaLanguage.INSTANCE); final int maxCommentLength = rightMargin - continuationPrefix.length(); final int firstLinePrefixLength = firstLinePrefix.length(); final boolean firstLineShorter = firstLinePrefixLength > continuationPrefix.length(); StringBuilder sb = new StringBuilder(firstLinePrefix); List<String> list; boolean canWrap = !mySettings.JD_PRESERVE_LINE_FEEDS || hasLineLongerThan(str, maxCommentLength); //If wrap comments selected, comments should be wrapped by the right margin if (myCommonSettings.WRAP_COMMENTS && canWrap) { list = toArrayWrapping(str, maxCommentLength); if (firstLineShorter && list != null && !list.isEmpty() && list.get(0).length() > rightMargin - firstLinePrefixLength) { list = new ArrayList<>(); //want the first line to be shorter, according to it's prefix String firstLine = toArrayWrapping(str, rightMargin - firstLinePrefixLength).get(0); //so now first line is exactly same width we need list.add(firstLine); str = str.substring(firstLine.length()); //actually there is one more problem - when first line has unclosed <pre> tag, substring should be processed if it's inside <pre> boolean unclosedPreTag = lineHasUnclosedPreTag(firstLine); if (unclosedPreTag) { str = PRE_TAG_START + str.replaceAll("^\\s+", ""); } //getting all another lines according to their prefix List<String> subList = toArrayWrapping(str, maxCommentLength); //removing pre tag if (unclosedPreTag && subList != null && !subList.isEmpty()) { String firstLineTagRemoved = subList.get(0).substring(PRE_TAG_START.length()); subList.set(0, firstLineTagRemoved); } if (subList != null) list.addAll(subList); } } else { list = toArray(str, new ArrayList<>()); } if (list == null) { sb.append('\n'); } else { int snippetBraceBalance = 0; boolean insidePreTag = false; for (int i = 0; i < list.size(); i++) { String line = list.get(i); if (line.isEmpty() && !mySettings.JD_KEEP_EMPTY_LINES) continue; if (i != 0) sb.append(continuationPrefix); if (line.isEmpty() && mySettings.JD_P_AT_EMPTY_LINES && !insidePreTag && !isFollowedByTagLine(list, i) && snippetBraceBalance == 0) { sb.append(P_START_TAG); } else { sb.append(line); // We want to track if we're inside <pre>...</pre> in order to not generate <p/> there. if (lineHasUnclosedPreTag(line)) { insidePreTag = true; } else if (lineHasClosingPreTag(line)) { insidePreTag = false; } if (snippetBraceBalance == 0) { if (lineHasUnclosedSnippetTag(line)) snippetBraceBalance = 1; } else { snippetBraceBalance += getLineSnippetTagBraceBalance(line); } } sb.append('\n'); } } return sb; } private static boolean isFollowedByTagLine(List<String> lines, int currLine) { for (int i = currLine + 1; i < lines.size(); i ++) { String line = lines.get(i); if (!line.isEmpty()) { return HtmlUtil.startsWithTag(line); } } return false; } private record CommentInfo(PsiDocComment docComment, PsiElement commentOwner, String commentHeader, String comment, String commentFooter) { } }