Bug 512095 - [formatter] Unstable wrap on a line with wrapped code and wrapped block comment
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterCommentsBugsTest.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterCommentsBugsTest.java
index 28ef2d7..4c6f07e 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterCommentsBugsTest.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterCommentsBugsTest.java
@@ -7577,4 +7577,26 @@
 		"}"
 	);
 }
+/**
+ * https://bugs.eclipse.org/512095 - [formatter] Unstable wrap on a line with wrapped code and wrapped block comment
+ */
+public void testBug512095() {
+	String source = 
+		"class Test1 {\n" + 
+		"	void f() {\n" + 
+		"		String c = \"aaaaaaaaaaaaaaaa\" + \"aaaaaaaaaaaaaaa\"; /* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 */\n" + 
+		"	}\n" + 
+		"}";
+	formatSource(source,
+		"class Test1 {\n" + 
+		"	void f() {\n" + 
+		"		String c = \"aaaaaaaaaaaaaaaa\"\n" + 
+		"				+ \"aaaaaaaaaaaaaaa\"; /*\n" + 
+		"										 * 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\n" + 
+		"										 * 16 17\n" + 
+		"										 */\n" + 
+		"	}\n" + 
+		"}"
+	);
+}
 }
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java
index 29b8007..014638a 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java
@@ -1008,11 +1008,14 @@
 		boolean newLinesAtBoundries = commentToken.tokenType == TokenNameCOMMENT_JAVADOC
 				? this.options.comment_new_lines_at_javadoc_boundaries
 				: this.options.comment_new_lines_at_block_boundaries;
-		if (newLinesAtBoundries && this.tm.countLineBreaksBetween(first, last) > 0) {
+		if (!newLinesAtBoundries) {
+			structure.get(1).clearLineBreaksBefore();
+			last.clearLineBreaksBefore();
+		} else if (this.tm.countLineBreaksBetween(first, last) > 0) {
 			first.breakAfter();
 			last.breakBefore();
-			last.setAlign(1);
 		}
+		last.setAlign(1);
 
 		if (structure.size() == 2)
 			return false;
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/CommentWrapExecutor.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/CommentWrapExecutor.java
index bd659f3..07333ee 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/CommentWrapExecutor.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/CommentWrapExecutor.java
@@ -36,9 +36,9 @@
 	private final ArrayList<Token> nlsTags = new ArrayList<>();
 
 	private int lineStartPosition;
-	private List<Token> blockStructure;
 	private boolean simulation;
 	private boolean wrapDisabled;
+	private boolean newLinesAtBoundries;
 
 	private Token potentialWrapToken, potentialWrapTokenSubstitute;
 	private int counterIfWrapped, counterIfWrappedSubstitute;
@@ -61,40 +61,26 @@
 	public int wrapMultiLineComment(Token commentToken, int startPosition, boolean simulate, boolean noWrap) {
 		this.lineCounter = 1;
 		this.counter = startPosition;
-
-		List<Token> structure = commentToken.getInternalStructure();
-		if (structure == null || structure.isEmpty())
-			return startPosition + this.tm.getLength(commentToken, startPosition);
-
 		commentToken.setIndent(this.tm.toIndent(startPosition, true));
 		this.lineStartPosition = commentToken.getIndent();
 		this.simulation = simulate;
 		this.wrapDisabled = noWrap;
 		this.potentialWrapToken = this.potentialWrapTokenSubstitute = null;
-		this.blockStructure = structure;
-		traverse(structure, 0);
-
-		boolean newLinesAtBoundries = commentToken.tokenType == TokenNameCOMMENT_JAVADOC
+		this.newLinesAtBoundries = commentToken.tokenType == TokenNameCOMMENT_JAVADOC
 				? this.options.comment_new_lines_at_javadoc_boundaries
 				: this.options.comment_new_lines_at_block_boundaries;
-		if (this.lineCounter > 1 && newLinesAtBoundries) {
-			Token endingToken = structure.get(structure.size() - 1);
-			if (!simulate && endingToken.tokenType != TokenNameNotAToken) {
-				structure.get(0).breakAfter();
-				endingToken.breakBefore();
-				endingToken.setAlign(1);
-			}
-			return startPosition + this.tm.getLength(endingToken, startPosition);
-		} else if (this.lineCounter > 1 && !newLinesAtBoundries) {
-			// the rest of this code assumes that newLinesAtBoundries==true, so now subtract the additional lines
-			this.lineCounter -= 2;
 
-			this.lineCounter -= structure.get(1).getLineBreaksBefore();
-			structure.get(1).clearLineBreaksBefore();
-			Token last = structure.get(structure.size() - 1);
-			this.lineCounter -= last.getLineBreaksBefore();
-			last.clearLineBreaksBefore();
-		}
+		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;
 	}
 
@@ -102,19 +88,58 @@
 		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.options.comment_line_length || 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) {
-		int positionIfNewLine = this.lineStartPosition + token.getAlign() + token.getIndent();
-		if (token.tokenType != TokenNameNotAToken)
-			positionIfNewLine += COMMENT_LINE_SEPARATOR_LENGTH;
-		if (getLineBreaksBefore() > 0) {
-			this.lineCounter = Math.max(this.lineCounter + getLineBreaksBefore(), 4);
+		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;
 
 			boolean isFormattedCode = token.getWrapPolicy() != null
 					&& token.getWrapPolicy() != WrapPolicy.SUBSTITUTE_ONLY;
-			if (!isFormattedCode && token.getAlign() == 0) {
+			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());
@@ -122,8 +147,7 @@
 			}
 		}
 
-		boolean canWrap = getNext() != null && getLineBreaksBefore() == 0 && index > 1
-				&& positionIfNewLine < this.counter;
+		boolean canWrap = getNext() != null && lineBreaksBefore == 0 && index > 1 && positionIfNewLine < this.counter;
 		if (canWrap) {
 			if (token.getWrapPolicy() == null) {
 				this.potentialWrapToken = token;
@@ -143,7 +167,6 @@
 				this.potentialWrapToken = this.potentialWrapTokenSubstitute;
 				this.counterIfWrapped = this.counterIfWrappedSubstitute;
 			}
-			this.counter = this.counterIfWrapped;
 			if (!this.simulation) {
 				this.potentialWrapToken.breakBefore();
 				// Indents are reserved for code inside <pre>.
@@ -151,7 +174,8 @@
 				this.potentialWrapToken.setAlign(this.potentialWrapToken.getIndent());
 				this.potentialWrapToken.setIndent(0);
 			}
-			this.lineCounter = Math.max(this.lineCounter + 1, 4);
+			this.counter = this.counterIfWrapped;
+			this.lineCounter++;
 			this.potentialWrapToken = this.potentialWrapTokenSubstitute = null;
 		}
 
@@ -178,31 +202,9 @@
 			this.potentialWrapToken = null;
 		}
 		if (this.potentialWrapToken == null && this.potentialWrapTokenSubstitute == null) {
-			boolean isFormattingEnabled = this.blockStructure.size() > 1
-					&& this.blockStructure.get(1).tokenType == TokenNameNotAToken;
-			if (isFormattingEnabled) {
-				// can't wrap, but the long comment cannot stay in one line
-				this.lineCounter = Math.max(this.lineCounter, 3);
-			}
 			return false;
 		}
 
-		if (this.options.comment_new_lines_at_javadoc_boundaries) {
-			if (getNext() == null) { // the closing token will go to the next line anyway
-				this.lineCounter = Math.max(this.lineCounter, 3);
-				return false;
-			}
-			if (this.lineCounter == 1) {
-				// when wrapping the first line of javadoc (more asterisks in opening token), the line will
-				// move to the left so it may not need wrapping in the end
-				int openingTokenLength = this.tm.getLength(this.blockStructure.get(0), 0);
-				if (this.counter - (openingTokenLength - 2) <= lineLenght) {
-					this.counter -= (openingTokenLength - 2);
-					this.lineCounter = Math.max(this.lineCounter, 3);
-					return false;
-				}
-			}
-		}
 		return true;
 	}