/******************************************************************************* * 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; } }