Copyright (c) 2014, 2018 Mateusz Matela and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: Mateusz Matela - [formatter] Formatter does not format Java code correctly, especially when max line width is set - https://bugs.eclipse.org/303519 Lars Vogel - Contributions for Bug 473178
/******************************************************************************* * Copyright (c) 2014, 2018 Mateusz Matela and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * Mateusz Matela <mateusz.matela@gmail.com> - [formatter] Formatter does not format Java code correctly, especially when max line width is set - https://bugs.eclipse.org/303519 * Lars Vogel <Lars.Vogel@vogella.com> - Contributions for * Bug 473178 *******************************************************************************/
package org.eclipse.jdt.internal.formatter.linewrap; import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_JAVADOC; import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_LINE; import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameNotAToken; import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameWHITESPACE; import static org.eclipse.jdt.internal.formatter.CommentsPreparator.COMMENT_LINE_SEPARATOR_LENGTH; import java.util.ArrayList; import java.util.List; import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions; import org.eclipse.jdt.internal.formatter.Token; import org.eclipse.jdt.internal.formatter.TokenManager; import org.eclipse.jdt.internal.formatter.TokenTraverser; import org.eclipse.jdt.internal.formatter.Token.WrapMode; import org.eclipse.jdt.internal.formatter.Token.WrapPolicy; public class CommentWrapExecutor extends TokenTraverser { private final TokenManager tm; private final DefaultCodeFormatterOptions options; private final ArrayList<Token> nlsTags = new ArrayList<>(); private int lineStartPosition; private int lineLimit; private boolean simulation; private boolean wrapDisabled; private boolean newLinesAtBoundries; private Token potentialWrapToken, potentialWrapTokenSubstitute; private int counterIfWrapped, counterIfWrappedSubstitute; private int lineCounter; public CommentWrapExecutor(TokenManager tokenManager, DefaultCodeFormatterOptions options) { this.tm = tokenManager; this.options = options; }
Params:
  • commentToken – token to wrap
  • startPosition – position in line of the beginning of the comment
  • simulate – if true, the properties of internal tokens will not really change. This mode is useful for checking how much space the comment takes.
  • noWrap – if true, it means that wrapping is disabled for this comment (for example because there's a NON-NLS tag after it). This method is still useful for checking comment length in that case.
Returns:position in line at the end of comment
/** * @param commentToken token to wrap * @param startPosition position in line of the beginning of the comment * @param simulate if {@code true}, the properties of internal tokens will not really change. This * mode is useful for checking how much space the comment takes. * @param noWrap if {@code true}, it means that wrapping is disabled for this comment (for example because there's * a NON-NLS tag after it). This method is still useful for checking comment length in that case. * @return position in line at the end of comment */
public int wrapMultiLineComment(Token commentToken, int startPosition, boolean simulate, boolean noWrap) { this.lineCounter = 1; this.counter = startPosition; commentToken.setIndent(this.tm.toIndent(startPosition, true)); this.lineStartPosition = commentToken.getIndent(); this.lineLimit = getLineLimit(startPosition); this.simulation = simulate; this.wrapDisabled = noWrap; this.potentialWrapToken = this.potentialWrapTokenSubstitute = null; this.newLinesAtBoundries = commentToken.tokenType == TokenNameCOMMENT_JAVADOC ? this.options.comment_new_lines_at_javadoc_boundaries : this.options.comment_new_lines_at_block_boundaries; List<Token> structure = commentToken.getInternalStructure(); if (structure == null || structure.isEmpty()) return startPosition + this.tm.getLength(commentToken, startPosition); int position = tryToFitInOneLine(structure, startPosition, noWrap); if (position > 0) return position; traverse(structure, 0); cleanupIndent(structure); if (this.newLinesAtBoundries) return this.lineStartPosition + 1 + this.tm.getLength(structure.get(structure.size() - 1), 0); return this.counter; } public int getLinesCount() { return this.lineCounter; } private int tryToFitInOneLine(List<Token> structure, int startPosition, boolean noWrap) { int position = startPosition; boolean hasWrapPotential = false; boolean wasSpaceAfter = false; for (int i = 0; i < structure.size(); i++) { Token token = structure.get(i); if (token.getLineBreaksBefore() > 0 || token.getLineBreaksAfter() > 0) { assert !noWrap; // comment already wrapped return -1; } if (!wasSpaceAfter && token.isSpaceBefore()) position++; position += this.tm.getLength(token, position); wasSpaceAfter = token.isSpaceAfter(); if (wasSpaceAfter) position++; WrapPolicy policy = token.getWrapPolicy(); if (i > 1 && (policy == null || policy == WrapPolicy.SUBSTITUTE_ONLY)) hasWrapPotential = true; } if (position <= this.lineLimit || noWrap || !hasWrapPotential) return position; return -1; } private int getStartingPosition(Token token, boolean isNewLine) { int position = this.lineStartPosition + token.getAlign() + (isNewLine ? token.getIndent() : 0); if (token.tokenType != TokenNameNotAToken) position += COMMENT_LINE_SEPARATOR_LENGTH; return position; } @Override protected boolean token(Token token, int index) { final int positionIfNewLine = getStartingPosition(token, true); int lineBreaksBefore = getLineBreaksBefore(); if ((index == 1 || getNext() == null) && this.newLinesAtBoundries && lineBreaksBefore == 0) { if (!this.simulation) token.breakBefore(); lineBreaksBefore = 1; } if (lineBreaksBefore > 0) { this.lineCounter += lineBreaksBefore; this.counter = positionIfNewLine; this.potentialWrapToken = this.potentialWrapTokenSubstitute = null; this.lineLimit = getLineLimit(this.lineStartPosition); } boolean canWrap = getNext() != null && lineBreaksBefore == 0 && index > 1 && positionIfNewLine < this.counter; if (canWrap) { if (token.getWrapPolicy() == null) { this.potentialWrapToken = token; this.counterIfWrapped = positionIfNewLine; } else if (token.getWrapPolicy() == WrapPolicy.SUBSTITUTE_ONLY) { this.potentialWrapTokenSubstitute = token; this.counterIfWrappedSubstitute = positionIfNewLine; } } if (index > 1 && getNext() != null && (token.getAlign() + token.getIndent()) > 0) this.counter = Math.max(this.counter, getStartingPosition(token, getLineBreaksBefore() > 0)); this.counter += this.tm.getLength(token, this.counter); this.counterIfWrapped += this.tm.getLength(token, this.counterIfWrapped); this.counterIfWrappedSubstitute += this.tm.getLength(token, this.counterIfWrappedSubstitute); if (shouldWrap()) { if (this.potentialWrapToken == null) { assert this.potentialWrapTokenSubstitute != null; this.potentialWrapToken = this.potentialWrapTokenSubstitute; this.counterIfWrapped = this.counterIfWrappedSubstitute; } if (!this.simulation) { this.potentialWrapToken.breakBefore(); } this.counter = this.counterIfWrapped; this.lineCounter++; this.potentialWrapToken = this.potentialWrapTokenSubstitute = null; this.lineLimit = getLineLimit(this.lineStartPosition); } if (isSpaceAfter()) { this.counter++; this.counterIfWrapped++; } return true; } private boolean shouldWrap() { if (this.wrapDisabled || this.counter <= this.lineLimit) return false; if (getLineBreaksAfter() == 0 && getNext() != null && getNext().getWrapPolicy() == WrapPolicy.DISABLE_WRAP) { // The next token cannot be wrapped, so there's no need to wrap now. // Let's wait and decide when there's more information available. return false; } if (this.potentialWrapToken != null && this.potentialWrapTokenSubstitute != null && this.counterIfWrapped > this.lineLimit && this.counterIfWrappedSubstitute < this.counterIfWrapped) { // there is a normal token to wrap, but the line would overflow anyway - better use substitute this.potentialWrapToken = null; } if (this.potentialWrapToken == null && this.potentialWrapTokenSubstitute == null) { return false; } return true; } private void cleanupIndent(List<Token> structure) { if (this.simulation) return; new TokenTraverser() { @Override protected boolean token(Token token, int index) { if (token.tokenType == TokenNameCOMMENT_JAVADOC && token.getInternalStructure() == null) { if (getLineBreaksBefore() > 0) token.setAlign(token.getAlign() + token.getIndent()); token.setIndent(0); } return true; } }.traverse(structure, 0); } public void wrapLineComment(Token commentToken, int startPosition) { List<Token> structure = commentToken.getInternalStructure(); if (structure == null || structure.isEmpty()) return; int commentIndex = this.tm.indexOf(commentToken); boolean isHeader = this.tm.isInHeader(commentIndex); boolean formattingEnabled = (this.options.comment_format_line_comment && !isHeader) || (this.options.comment_format_header && isHeader); if (!formattingEnabled) return; int position = startPosition; startPosition = this.tm.toIndent(startPosition, true); int indent = startPosition; int limit = getLineLimit(position); for (Token token : structure) { if (token.hasNLSTag()) { this.nlsTags.add(token); position += token.countChars() + (token.isSpaceBefore() ? 1 : 0); } } Token whitespace = null; Token prefix = structure.get(0); if (prefix.tokenType == TokenNameWHITESPACE) { whitespace = new Token(prefix); whitespace.breakBefore(); whitespace.setIndent(indent); whitespace.setWrapPolicy(new WrapPolicy(WrapMode.WHERE_NECESSARY, commentIndex, 0)); prefix = structure.get(1); assert prefix.tokenType == TokenNameCOMMENT_LINE; } int prefixEnd = commentToken.originalStart + 1; if (!prefix.hasNLSTag()) prefixEnd = Math.max(prefixEnd, prefix.originalEnd); // comments can start with more than 2 slashes prefix = new Token(commentToken.originalStart, prefixEnd, TokenNameCOMMENT_LINE); if (whitespace == null) { prefix.breakBefore(); prefix.setWrapPolicy(new WrapPolicy(WrapMode.WHERE_NECESSARY, commentIndex, 0)); } int lineStartIndex = whitespace == null ? 0 : 1; for (int i = 0; i < structure.size(); i++) { Token token = structure.get(i); token.setIndent(indent); if (token.hasNLSTag()) { this.nlsTags.remove(token); continue; } if (token.isSpaceBefore()) position++; if (token.getLineBreaksBefore() > 0) { position = startPosition; limit = getLineLimit(position); lineStartIndex = whitespace == null ? i : i + 1; if (whitespace != null && token != whitespace) { token.clearLineBreaksBefore(); structure.add(i, whitespace); token = whitespace; } } position += this.tm.getLength(token, position); if (token.tokenType == TokenNameWHITESPACE) limit = getLineLimit(position); if (position > limit && i > lineStartIndex + 1) { structure.add(i, prefix); if (whitespace != null) structure.add(i, whitespace); structure.removeAll(this.nlsTags); structure.addAll(i, this.nlsTags); i = i + this.nlsTags.size() - 1; this.nlsTags.clear(); } } this.nlsTags.clear(); } private int getLineLimit(int startPosition) { final int commentLength = this.options.comment_line_length; if (!this.options.comment_count_line_length_from_starting_position) return commentLength; final int pageWidth = this.options.page_width; int lineLength = startPosition + commentLength; if (lineLength > pageWidth && commentLength <= pageWidth) lineLength = pageWidth; return lineLength; } }