blob: 3923553f6044492a47379e7428a095ccee0ee1bb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2017 Mateusz Matela and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* 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;
}
/**
* @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);
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) {
int position = this.lineStartPosition + token.getAlign() + token.getIndent();
if (token.tokenType != TokenNameNotAToken)
position += COMMENT_LINE_SEPARATOR_LENGTH;
return position;
}
@Override
protected boolean token(Token token, int index) {
final int positionIfNewLine = getStartingPosition(token);
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 isFormattedCode = token.getWrapPolicy() != null
&& token.getWrapPolicy() != WrapPolicy.SUBSTITUTE_ONLY;
if (!isFormattedCode && token.getAlign() == 0 && !this.simulation) {
// Indents are reserved for code inside <pre>.
// Indentation of javadoc tags can be achieved with align
token.setAlign(token.getIndent());
token.setIndent(0);
}
}
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;
}
}
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();
// Indents are reserved for code inside <pre>.
// Indentation of javadoc tags can be achieved with align
this.potentialWrapToken.setAlign(this.potentialWrapToken.getIndent());
this.potentialWrapToken.setIndent(0);
}
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;
}
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;
}
}