1 /******************************************************************************* 2 * Copyright (c) 2014, 2018 Mateusz Matela and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * 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 13 * Lars Vogel <Lars.Vogel@vogella.com> - Contributions for 14 * Bug 473178 15 *******************************************************************************/ 16 package org.eclipse.jdt.internal.formatter.linewrap; 17 18 import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_JAVADOC; 19 import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_LINE; 20 import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameNotAToken; 21 import static org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameWHITESPACE; 22 import static org.eclipse.jdt.internal.formatter.CommentsPreparator.COMMENT_LINE_SEPARATOR_LENGTH; 23 24 import java.util.ArrayList; 25 import java.util.List; 26 27 import org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions; 28 import org.eclipse.jdt.internal.formatter.Token; 29 import org.eclipse.jdt.internal.formatter.TokenManager; 30 import org.eclipse.jdt.internal.formatter.TokenTraverser; 31 import org.eclipse.jdt.internal.formatter.Token.WrapMode; 32 import org.eclipse.jdt.internal.formatter.Token.WrapPolicy; 33 34 public class CommentWrapExecutor extends TokenTraverser { 35 36 private final TokenManager tm; 37 private final DefaultCodeFormatterOptions options; 38 39 private final ArrayList<Token> nlsTags = new ArrayList<>(); 40 41 private int lineStartPosition; 42 private int lineLimit; 43 private boolean simulation; 44 private boolean wrapDisabled; 45 private boolean newLinesAtBoundries; 46 47 private Token potentialWrapToken, potentialWrapTokenSubstitute; 48 private int counterIfWrapped, counterIfWrappedSubstitute; 49 private int lineCounter; 50 CommentWrapExecutor(TokenManager tokenManager, DefaultCodeFormatterOptions options)51 public CommentWrapExecutor(TokenManager tokenManager, DefaultCodeFormatterOptions options) { 52 this.tm = tokenManager; 53 this.options = options; 54 } 55 56 /** 57 * @param commentToken token to wrap 58 * @param startPosition position in line of the beginning of the comment 59 * @param simulate if {@code true}, the properties of internal tokens will not really change. This 60 * mode is useful for checking how much space the comment takes. 61 * @param noWrap if {@code true}, it means that wrapping is disabled for this comment (for example because there's 62 * a NON-NLS tag after it). This method is still useful for checking comment length in that case. 63 * @return position in line at the end of comment 64 */ wrapMultiLineComment(Token commentToken, int startPosition, boolean simulate, boolean noWrap)65 public int wrapMultiLineComment(Token commentToken, int startPosition, boolean simulate, boolean noWrap) { 66 this.lineCounter = 1; 67 this.counter = startPosition; 68 commentToken.setIndent(this.tm.toIndent(startPosition, true)); 69 this.lineStartPosition = commentToken.getIndent(); 70 this.lineLimit = getLineLimit(startPosition); 71 this.simulation = simulate; 72 this.wrapDisabled = noWrap; 73 this.potentialWrapToken = this.potentialWrapTokenSubstitute = null; 74 this.newLinesAtBoundries = commentToken.tokenType == TokenNameCOMMENT_JAVADOC 75 ? this.options.comment_new_lines_at_javadoc_boundaries 76 : this.options.comment_new_lines_at_block_boundaries; 77 78 List<Token> structure = commentToken.getInternalStructure(); 79 if (structure == null || structure.isEmpty()) 80 return startPosition + this.tm.getLength(commentToken, startPosition); 81 82 int position = tryToFitInOneLine(structure, startPosition, noWrap); 83 if (position > 0) 84 return position; 85 86 traverse(structure, 0); 87 cleanupIndent(structure); 88 89 if (this.newLinesAtBoundries) 90 return this.lineStartPosition + 1 + this.tm.getLength(structure.get(structure.size() - 1), 0); 91 return this.counter; 92 } 93 getLinesCount()94 public int getLinesCount() { 95 return this.lineCounter; 96 } 97 tryToFitInOneLine(List<Token> structure, int startPosition, boolean noWrap)98 private int tryToFitInOneLine(List<Token> structure, int startPosition, boolean noWrap) { 99 int position = startPosition; 100 boolean hasWrapPotential = false; 101 boolean wasSpaceAfter = false; 102 for (int i = 0; i < structure.size(); i++) { 103 Token token = structure.get(i); 104 if (token.getLineBreaksBefore() > 0 || token.getLineBreaksAfter() > 0) { 105 assert !noWrap; // comment already wrapped 106 return -1; 107 } 108 if (!wasSpaceAfter && token.isSpaceBefore()) 109 position++; 110 position += this.tm.getLength(token, position); 111 wasSpaceAfter = token.isSpaceAfter(); 112 if (wasSpaceAfter) 113 position++; 114 115 WrapPolicy policy = token.getWrapPolicy(); 116 if (i > 1 && (policy == null || policy == WrapPolicy.SUBSTITUTE_ONLY)) 117 hasWrapPotential = true; 118 } 119 if (position <= this.lineLimit || noWrap || !hasWrapPotential) 120 return position; 121 return -1; 122 } 123 getStartingPosition(Token token, boolean isNewLine)124 private int getStartingPosition(Token token, boolean isNewLine) { 125 int position = this.lineStartPosition + token.getAlign() + (isNewLine ? token.getIndent() : 0); 126 if (token.tokenType != TokenNameNotAToken) 127 position += COMMENT_LINE_SEPARATOR_LENGTH; 128 return position; 129 } 130 131 @Override token(Token token, int index)132 protected boolean token(Token token, int index) { 133 final int positionIfNewLine = getStartingPosition(token, true); 134 135 int lineBreaksBefore = getLineBreaksBefore(); 136 if ((index == 1 || getNext() == null) && this.newLinesAtBoundries && lineBreaksBefore == 0) { 137 if (!this.simulation) 138 token.breakBefore(); 139 lineBreaksBefore = 1; 140 } 141 142 if (lineBreaksBefore > 0) { 143 this.lineCounter += lineBreaksBefore; 144 this.counter = positionIfNewLine; 145 this.potentialWrapToken = this.potentialWrapTokenSubstitute = null; 146 this.lineLimit = getLineLimit(this.lineStartPosition); 147 148 } 149 150 boolean canWrap = getNext() != null && lineBreaksBefore == 0 && index > 1 && positionIfNewLine < this.counter; 151 if (canWrap) { 152 if (token.getWrapPolicy() == null) { 153 this.potentialWrapToken = token; 154 this.counterIfWrapped = positionIfNewLine; 155 } else if (token.getWrapPolicy() == WrapPolicy.SUBSTITUTE_ONLY) { 156 this.potentialWrapTokenSubstitute = token; 157 this.counterIfWrappedSubstitute = positionIfNewLine; 158 } 159 } 160 161 if (index > 1 && getNext() != null && (token.getAlign() + token.getIndent()) > 0) 162 this.counter = Math.max(this.counter, getStartingPosition(token, getLineBreaksBefore() > 0)); 163 this.counter += this.tm.getLength(token, this.counter); 164 this.counterIfWrapped += this.tm.getLength(token, this.counterIfWrapped); 165 this.counterIfWrappedSubstitute += this.tm.getLength(token, this.counterIfWrappedSubstitute); 166 if (shouldWrap()) { 167 if (this.potentialWrapToken == null) { 168 assert this.potentialWrapTokenSubstitute != null; 169 this.potentialWrapToken = this.potentialWrapTokenSubstitute; 170 this.counterIfWrapped = this.counterIfWrappedSubstitute; 171 } 172 if (!this.simulation) { 173 this.potentialWrapToken.breakBefore(); 174 } 175 this.counter = this.counterIfWrapped; 176 this.lineCounter++; 177 this.potentialWrapToken = this.potentialWrapTokenSubstitute = null; 178 this.lineLimit = getLineLimit(this.lineStartPosition); 179 } 180 181 if (isSpaceAfter()) { 182 this.counter++; 183 this.counterIfWrapped++; 184 } 185 186 return true; 187 } 188 shouldWrap()189 private boolean shouldWrap() { 190 if (this.wrapDisabled || this.counter <= this.lineLimit) 191 return false; 192 if (getLineBreaksAfter() == 0 && getNext() != null && getNext().getWrapPolicy() == WrapPolicy.DISABLE_WRAP) { 193 // The next token cannot be wrapped, so there's no need to wrap now. 194 // Let's wait and decide when there's more information available. 195 return false; 196 } 197 if (this.potentialWrapToken != null && this.potentialWrapTokenSubstitute != null 198 && this.counterIfWrapped > this.lineLimit && this.counterIfWrappedSubstitute < this.counterIfWrapped) { 199 // there is a normal token to wrap, but the line would overflow anyway - better use substitute 200 this.potentialWrapToken = null; 201 } 202 if (this.potentialWrapToken == null && this.potentialWrapTokenSubstitute == null) { 203 return false; 204 } 205 206 return true; 207 } 208 cleanupIndent(List<Token> structure)209 private void cleanupIndent(List<Token> structure) { 210 if (this.simulation) 211 return; 212 new TokenTraverser() { 213 @Override 214 protected boolean token(Token token, int index) { 215 if (token.tokenType == TokenNameCOMMENT_JAVADOC && token.getInternalStructure() == null) { 216 if (getLineBreaksBefore() > 0) 217 token.setAlign(token.getAlign() + token.getIndent()); 218 token.setIndent(0); 219 } 220 return true; 221 } 222 }.traverse(structure, 0); 223 } 224 wrapLineComment(Token commentToken, int startPosition)225 public void wrapLineComment(Token commentToken, int startPosition) { 226 List<Token> structure = commentToken.getInternalStructure(); 227 if (structure == null || structure.isEmpty()) 228 return; 229 int commentIndex = this.tm.indexOf(commentToken); 230 boolean isHeader = this.tm.isInHeader(commentIndex); 231 boolean formattingEnabled = (this.options.comment_format_line_comment && !isHeader) 232 || (this.options.comment_format_header && isHeader); 233 if (!formattingEnabled) 234 return; 235 236 int position = startPosition; 237 startPosition = this.tm.toIndent(startPosition, true); 238 int indent = startPosition; 239 int limit = getLineLimit(position); 240 241 for (Token token : structure) { 242 if (token.hasNLSTag()) { 243 this.nlsTags.add(token); 244 position += token.countChars() + (token.isSpaceBefore() ? 1 : 0); 245 } 246 } 247 248 Token whitespace = null; 249 Token prefix = structure.get(0); 250 if (prefix.tokenType == TokenNameWHITESPACE) { 251 whitespace = new Token(prefix); 252 whitespace.breakBefore(); 253 whitespace.setIndent(indent); 254 whitespace.setWrapPolicy(new WrapPolicy(WrapMode.WHERE_NECESSARY, commentIndex, 0)); 255 prefix = structure.get(1); 256 assert prefix.tokenType == TokenNameCOMMENT_LINE; 257 } 258 int prefixEnd = commentToken.originalStart + 1; 259 if (!prefix.hasNLSTag()) 260 prefixEnd = Math.max(prefixEnd, prefix.originalEnd); // comments can start with more than 2 slashes 261 prefix = new Token(commentToken.originalStart, prefixEnd, TokenNameCOMMENT_LINE); 262 if (whitespace == null) { 263 prefix.breakBefore(); 264 prefix.setWrapPolicy(new WrapPolicy(WrapMode.WHERE_NECESSARY, commentIndex, 0)); 265 } 266 267 int lineStartIndex = whitespace == null ? 0 : 1; 268 for (int i = 0; i < structure.size(); i++) { 269 Token token = structure.get(i); 270 token.setIndent(indent); 271 if (token.hasNLSTag()) { 272 this.nlsTags.remove(token); 273 continue; 274 } 275 if (token.isSpaceBefore()) 276 position++; 277 if (token.getLineBreaksBefore() > 0) { 278 position = startPosition; 279 limit = getLineLimit(position); 280 lineStartIndex = whitespace == null ? i : i + 1; 281 if (whitespace != null && token != whitespace) { 282 token.clearLineBreaksBefore(); 283 structure.add(i, whitespace); 284 token = whitespace; 285 } 286 } 287 position += this.tm.getLength(token, position); 288 if (token.tokenType == TokenNameWHITESPACE) 289 limit = getLineLimit(position); 290 if (position > limit && i > lineStartIndex + 1) { 291 structure.add(i, prefix); 292 if (whitespace != null) 293 structure.add(i, whitespace); 294 295 structure.removeAll(this.nlsTags); 296 structure.addAll(i, this.nlsTags); 297 i = i + this.nlsTags.size() - 1; 298 this.nlsTags.clear(); 299 } 300 } 301 this.nlsTags.clear(); 302 } 303 getLineLimit(int startPosition)304 private int getLineLimit(int startPosition) { 305 final int commentLength = this.options.comment_line_length; 306 if (!this.options.comment_count_line_length_from_starting_position) 307 return commentLength; 308 final int pageWidth = this.options.page_width; 309 int lineLength = startPosition + commentLength; 310 if (lineLength > pageWidth && commentLength <= pageWidth) 311 lineLength = pageWidth; 312 return lineLength; 313 } 314 } 315