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