Bug 472815 - [formatter] 'Indent Empty lines' option doesn't work inside empty blocks

Change-Id: I938313d2cdce400420075dbaeb1b4163baf38ce7
Signed-off-by: Mateusz Matela <mateusz.matela@gmail.com>
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterBugsTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterBugsTests.java
index b4f540d..a846069 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterBugsTests.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterBugsTests.java
@@ -11463,4 +11463,163 @@
 		"}";

 	formatSource(source);

 }

+/**

+ * https://bugs.eclipse.org/472815 - [formatter] 'Indent Empty lines' option doesn't work inside empty blocks

+ */

+public void testBug472815() {

+	this.formatterPrefs.number_of_empty_lines_to_preserve = 2;

+	String source = 

+		"public class Snippet {\r\n" + 

+		"\r\n" + 

+		"	int[] a1 = { };\r\n" + 

+		"	int[] a2 = {\r\n" + 

+		"	};\r\n" + 

+		"	int[] a3 = {\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"	int[] a4 = {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"	int[] a5 = {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"\r\n" + 

+		"	void f1() { }\r\n" + 

+		"	void f2() {\r\n" + 

+		"	}\r\n" + 

+		"	void f3() {\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"	void f4() {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"	void f5() {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"}";

+	formatSource(source,

+		"public class Snippet {\r\n" + 

+		"\r\n" + 

+		"	int[] a1 = {};\r\n" + 

+		"	int[] a2 = {};\r\n" + 

+		"	int[] a3 = {\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"	int[] a4 = {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"	int[] a5 = {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"\r\n" + 

+		"	void f1() {\r\n" + 

+		"	}\r\n" + 

+		"\r\n" + 

+		"	void f2() {\r\n" + 

+		"	}\r\n" + 

+		"\r\n" + 

+		"	void f3() {\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"\r\n" + 

+		"	void f4() {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"\r\n" + 

+		"	void f5() {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"}"

+	);

+}

+/**

+ * https://bugs.eclipse.org/472815 - [formatter] 'Indent Empty lines' option doesn't work inside empty blocks

+ */

+public void testBug472815b() {

+	this.formatterPrefs.number_of_empty_lines_to_preserve = 2;

+	this.formatterPrefs.indent_empty_lines = true;

+	String source = 

+		"public class Snippet {\r\n" + 

+		"\r\n" + 

+		"	int[] a1 = { };\r\n" + 

+		"	int[] a2 = {\r\n" + 

+		"	};\r\n" + 

+		"	int[] a3 = {\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"	int[] a4 = {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"	int[] a5 = {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	};\r\n" + 

+		"\r\n" + 

+		"	void f1() { }\r\n" + 

+		"	void f2() {\r\n" + 

+		"	}\r\n" + 

+		"	void f3() {\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"	void f4() {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"	void f5() {\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"\r\n" + 

+		"	}\r\n" + 

+		"}";

+	formatSource(source,

+		"public class Snippet {\r\n" + 

+		"	\r\n" + 

+		"	int[] a1 = {};\r\n" + 

+		"	int[] a2 = {};\r\n" + 

+		"	int[] a3 = {\r\n" + 

+		"			\r\n" + 

+		"	};\r\n" + 

+		"	int[] a4 = {\r\n" + 

+		"			\r\n" + 

+		"			\r\n" + 

+		"	};\r\n" + 

+		"	int[] a5 = {\r\n" + 

+		"			\r\n" + 

+		"			\r\n" + 

+		"	};\r\n" + 

+		"	\r\n" + 

+		"	void f1() {\r\n" + 

+		"	}\r\n" + 

+		"	\r\n" + 

+		"	void f2() {\r\n" + 

+		"	}\r\n" + 

+		"	\r\n" + 

+		"	void f3() {\r\n" + 

+		"		\r\n" + 

+		"	}\r\n" + 

+		"	\r\n" + 

+		"	void f4() {\r\n" + 

+		"		\r\n" + 

+		"		\r\n" + 

+		"	}\r\n" + 

+		"	\r\n" + 

+		"	void f5() {\r\n" + 

+		"		\r\n" + 

+		"		\r\n" + 

+		"	}\r\n" + 

+		"}"

+	);

+}

 }

diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/LineBreaksPreparator.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/LineBreaksPreparator.java
index ae1ef58..92b756b 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/LineBreaksPreparator.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/LineBreaksPreparator.java
@@ -361,27 +361,28 @@
 	@Override
 	public boolean visit(ArrayInitializer node) {
 		int openBraceIndex = this.tm.firstIndexIn(node, TokenNameLBRACE);
-		Token afterOpenBraceToken = this.tm.get(openBraceIndex + 1);
-		boolean isEmpty = afterOpenBraceToken.tokenType == TokenNameRBRACE;
-		if (isEmpty && this.options.keep_empty_array_initializer_on_one_line)
-			return true;
+		boolean isEmpty = handleEmptyLinesIndentation(openBraceIndex);
 
-		Token openBraceToken = this.tm.get(openBraceIndex);
 		int closeBraceIndex = this.tm.lastIndexIn(node, TokenNameRBRACE);
-		handleBracePosition(openBraceToken, closeBraceIndex, this.options.brace_position_for_array_initializer);
+		Token openBraceToken = this.tm.get(openBraceIndex);
+		Token closeBraceToken = this.tm.get(closeBraceIndex);
+
+		if (!(node.getParent() instanceof ArrayInitializer)) {
+			Token afterOpenBraceToken = this.tm.get(openBraceIndex + 1);
+			for (int i = 0; i < this.options.continuation_indentation_for_array_initializer; i++) {
+				afterOpenBraceToken.indent();
+				closeBraceToken.unindent();
+			}
+		}
+
+		if (!isEmpty || !this.options.keep_empty_array_initializer_on_one_line)
+			handleBracePosition(openBraceToken, closeBraceIndex, this.options.brace_position_for_array_initializer);
 
 		if (!isEmpty) {
-			Token closeBraceToken = this.tm.get(closeBraceIndex);
 			if (this.options.insert_new_line_after_opening_brace_in_array_initializer)
 				openBraceToken.breakAfter();
 			if (this.options.insert_new_line_before_closing_brace_in_array_initializer)
 				closeBraceToken.breakBefore();
-			if (!(node.getParent() instanceof ArrayInitializer)) {
-				for (int i = 0; i < this.options.continuation_indentation_for_array_initializer; i++) {
-					afterOpenBraceToken.indent();
-					closeBraceToken.unindent();
-				}
-			}
 		}
 		return true;
 	}
@@ -550,6 +551,7 @@
 				: this.tm.firstIndexAfter(nodeBeforeOpenBrace, TokenNameLBRACE);
 		int closeBraceIndex = this.tm.lastIndexIn(node, TokenNameRBRACE);
 		Token openBraceToken = this.tm.get(openBraceIndex);
+		Token closeBraceToken = this.tm.get(closeBraceIndex);
 		handleBracePosition(openBraceToken, closeBraceIndex, bracePosition);
 
 		boolean isEmpty = true;
@@ -559,13 +561,16 @@
 				break;
 			}
 		}
+
+		handleEmptyLinesIndentation(openBraceIndex);
+
 		if (!isEmpty || newLineInEmpty) {
 			openBraceToken.breakAfter();
-			this.tm.get(closeBraceIndex).breakBefore();
+			closeBraceToken.breakBefore();
 		}
 		if (indentBody) {
 			this.tm.get(openBraceIndex + 1).indent();
-			this.tm.get(closeBraceIndex).unindent();
+			closeBraceToken.unindent();
 		}
 	}
 
@@ -582,6 +587,31 @@
 		}
 	}
 
+	private boolean handleEmptyLinesIndentation(int openBraceIndex) {
+		Token open = this.tm.get(openBraceIndex);
+		Token next = this.tm.get(openBraceIndex + 1);
+		boolean isEmpty = next.tokenType == TokenNameRBRACE;
+		if (!isEmpty || this.tm.countLineBreaksBetween(open, next) < 2 || !this.options.indent_empty_lines)
+			return isEmpty;
+
+		// find a line break and make a token out of it
+		for (int i = open.originalEnd + 1; i < next.originalStart; i++) {
+			char c = this.tm.charAt(i);
+			char c2 = this.tm.charAt(i + 1);
+			int lineBreakStart = (c == '\r' || c == '\n') ? i : -1;
+			int lineBreakEnd = ((c2 == '\r' || c2 == '\n') && c2 != c) ? i + 1 : lineBreakStart;
+			if (lineBreakStart >= 0) {
+				Token emptyLineToken = new Token(lineBreakStart, lineBreakEnd, Token.TokenNameEMPTY_LINE);
+				emptyLineToken.breakBefore();
+				emptyLineToken.breakAfter();
+				emptyLineToken.setToEscape(true); // force text builder to use toString()
+				this.tm.insert(openBraceIndex + 1, emptyLineToken);
+				return true;
+			}
+		}
+		return true;
+	}
+
 	private void indent(ASTNode node) {
 		int startIndex = this.tm.firstIndexIn(node, -1);
 		while (startIndex > 0 && this.tm.get(startIndex - 1).isComment())
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/Token.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/Token.java
index 66dfafb..762edd0 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/Token.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/Token.java
@@ -70,6 +70,9 @@
 		}
 	}
 
+	/** Special token type used to mark tokens that store empty line indentation */
+	public static final int TokenNameEMPTY_LINE = 10000;
+
 	/** Position in source of the first character. */
 	public final int originalStart;
 	/** Position in source of the last character (this position is included in the token). */
@@ -276,6 +279,8 @@
 	}
 
 	public String toString(String source) {
+		if (this.tokenType == TokenNameEMPTY_LINE)
+			return ""; //$NON-NLS-1$
 		return source.substring(this.originalStart, this.originalEnd + 1);
 	}
 
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/WrapPreparator.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/WrapPreparator.java
index 4d70102..51c8b38 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/WrapPreparator.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/linewrap/WrapPreparator.java
@@ -740,45 +740,25 @@
 
 			@Override
 			protected boolean token(Token token, int index) {
-				int lineBreaks = getLineBreaksBetween(getPrevious(), token);
-				if (index > WrapPreparator.this.importsStart && index < WrapPreparator.this.importsEnd) {
-					lineBreaks = lineBreaks > 1 ? (this.options2.blank_lines_between_import_groups + 1) : 0;
-				} else {
-					lineBreaks = Math.min(lineBreaks, this.options2.number_of_empty_lines_to_preserve + 1);
-				}
+				boolean isBetweenImports = index > WrapPreparator.this.importsStart && index < WrapPreparator.this.importsEnd;
+				int lineBreaks = getLineBreaksToPreserve(getPrevious(), token, isBetweenImports);
 				if (lineBreaks <= getLineBreaksBefore())
 					return true;
 
-				if (!this.options2.join_wrapped_lines && token.isWrappable() && lineBreaks == 1) {
-					token.breakBefore();
+				if (lineBreaks == 1) {
+					if ((!this.options2.join_wrapped_lines && token.isWrappable()) || index == 0)
+						token.breakBefore();
 				} else if (lineBreaks > 1) {
-					if (index == 0)
-						lineBreaks--;
 					token.putLineBreaksBefore(lineBreaks);
 				}
 				return true;
 			}
 
-			private int getLineBreaksBetween(Token token1, Token token2) {
-				if (token1 != null) {
-					List<Token> structure1 = token1.getInternalStructure();
-					if (structure1 != null && !structure1.isEmpty())
-						token1 = structure1.get(structure1.size() - 1);
-				}
-				List<Token> structure2 = token2.getInternalStructure();
-				if (structure2 != null && !structure2.isEmpty())
-					token2 = structure2.get(0);
-				int lineBreaks = WrapPreparator.this.tm.countLineBreaksBetween(token1, token2);
-				if (token1 == null)
-					lineBreaks++;
-				return lineBreaks;
-			}
 		});
 
 		Token last = this.tm.get(this.tm.size() - 1);
 		last.clearLineBreaksAfter();
-		int endingBreaks = this.tm.countLineBreaksBetween(last, null);
-		endingBreaks = Math.min(endingBreaks, this.options.number_of_empty_lines_to_preserve);
+		int endingBreaks = getLineBreaksToPreserve(last, null, false);
 		if (endingBreaks > 0) {
 			last.putLineBreaksAfter(endingBreaks);
 		} else if ((this.kind & CodeFormatter.K_COMPILATION_UNIT) != 0
@@ -787,6 +767,29 @@
 		}
 	}
 
+	int getLineBreaksToPreserve(Token token1, Token token2, boolean isBetweenImports) {
+		if (token1 != null) {
+			List<Token> structure = token1.getInternalStructure();
+			if (structure != null && !structure.isEmpty())
+				token1 = structure.get(structure.size() - 1);
+		}
+		if (token2 != null) {
+			List<Token> structure = token2.getInternalStructure();
+			if (structure != null && !structure.isEmpty())
+				token2 = structure.get(0);
+		}
+		int lineBreaks = WrapPreparator.this.tm.countLineBreaksBetween(token1, token2);
+		if (isBetweenImports)
+			return lineBreaks > 1 ? (this.options.blank_lines_between_import_groups + 1) : 0;
+
+		int toPreserve = this.options.number_of_empty_lines_to_preserve;
+		if (token1 != null && token2 != null)
+			toPreserve++; // n empty lines = n+1 line breaks, except for file start and end
+		if (token1 != null && token1.tokenType == Token.TokenNameEMPTY_LINE)
+			toPreserve--;
+		return Math.min(lineBreaks, toPreserve);
+	}
+
 	private void wrapComments() {
 		CommentWrapExecutor commentWrapper = new CommentWrapExecutor(this.tm, this.options);
 		boolean isNLSTagInLine = false;