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