Bug 553282 - [console] Handle \f and \v in console output

Change-Id: I8c86ad1116aa12c59ec4cbfbf2662e484c55c1c4
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java
index 865561f..831f810 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2019 Paul Pazderski and others.
+ * Copyright (c) 2019, 2020 Paul Pazderski and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -109,7 +109,7 @@
 	 */
 	public IOConsoleTestUtil clear() throws Exception {
 		console.clearConsole();
-		waitForScheduledJobs();
+		flush();
 		TestCase.assertEquals("Console is not cleared.", 0, doc.getLength());
 		return this;
 	}
@@ -121,6 +121,7 @@
 	 * @param s content to write in output stream
 	 * @return this {@link IOConsoleTestUtil} to chain methods
 	 * @see #write(String)
+	 * @see #flush()
 	 */
 	@SuppressWarnings("resource")
 	public IOConsoleTestUtil writeFast(final String s) throws IOException {
@@ -164,6 +165,7 @@
 	 * @param out use this output stream instead of default one
 	 * @return this {@link IOConsoleTestUtil} to chain methods
 	 * @see #write(String, IOConsoleOutputStream)
+	 * @see #flush()
 	 */
 	public IOConsoleTestUtil writeFast(final String s, IOConsoleOutputStream out) throws IOException {
 		out.write(s);
@@ -181,7 +183,7 @@
 	 */
 	public IOConsoleTestUtil write(final String s, IOConsoleOutputStream out) throws Exception {
 		writeFast(s, out);
-		waitForScheduledJobs();
+		flush();
 		return this;
 	}
 
@@ -543,7 +545,7 @@
 			verifyContentByOffset(expectedContent, line.getOffset());
 			TestCase.assertEquals("Line " + l + " has wrong length.", expectedContent.length(), line.getLength());
 		} catch (BadLocationException e) {
-			TestCase.fail("Expected line not found in console document. Bad location!");
+			TestCase.fail("Expected line " + lineNum + " not found in console document. Bad location!");
 		}
 		return this;
 	}
@@ -562,7 +564,7 @@
 			final int len = Math.min(doc.getLength() - o, expectedContent.length());
 			TestCase.assertEquals("Expected string not found in console document.", expectedContent, doc.get(o, len));
 		} catch (BadLocationException ex) {
-			TestCase.fail("Expected string not found in console document. Bad location!");
+			TestCase.fail("Expected string '" + expectedContent + "' not found in console document. Bad location!");
 		}
 		return this;
 	}
@@ -704,6 +706,18 @@
 	}
 
 	/**
+	 * Ensure all pending write operations are fully applied on the console view
+	 * before returning.
+	 *
+	 * @return this {@link IOConsoleTestUtil} to chain methods
+	 */
+	public IOConsoleTestUtil flush() {
+		// overall this method is just a better name for waitForScheduledJobs
+		waitForScheduledJobs();
+		return this;
+	}
+
+	/**
 	 * Close the default output stream if it was used.
 	 */
 	public void closeOutputStream() {
@@ -747,7 +761,9 @@
 	/**
 	 * If <code>true</code> the util will work as if console is not in fixed
 	 * width mode. E.g. {@link #moveCaretToLineStart()} will move caret to
-	 * document line start not to widget line start.
+	 * document line start not to widget line start or
+	 * {@link #verifyContentByLine(String, int)} would check the line as seen
+	 * without fixed width.
 	 *
 	 * @see #ignoreFixedConsole
 	 */
@@ -756,11 +772,12 @@
 	}
 
 	/**
-	 * Enable compatibility mode. If set to <code>true</code> written for
+	 * Enable compatibility mode. If set to <code>true</code> tests written for
 	 * console without fixed width should work with any fixed width. Commands
 	 * like {@link #moveCaretToLineStart()} are modified to not move to begin of
 	 * widget line (maybe wrapped line) but to start it would have without fixed
-	 * width.
+	 * width or {@link #verifyContentByLine(String, int)} would check the line
+	 * as seen without fixed width.
 	 *
 	 * @see #ignoreFixedConsole
 	 */
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
index bd18bd8..5ec28c6 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
@@ -195,7 +195,7 @@
 
 		c.writeAndVerify("Hello World!");
 		c.getDocument().replace(0, c.getContentLength(), "");
-		c.waitForScheduledJobs();
+		c.flush();
 		c.verifyContent("").verifyPartitions();
 
 		c.writeAndVerify("New console content.");
@@ -558,6 +558,49 @@
 	}
 
 	/**
+	 * Test handling of <code>\f</code>.
+	 */
+	public void testFormFeedControlCharacter() throws Exception {
+		final IOConsoleTestUtil c = getTestUtil("Test \\f");
+		c.getConsole().setHandleControlCharacters(true);
+		try (IOConsoleOutputStream err = c.getConsole().newOutputStream()) {
+			c.write("\f");
+			assertEquals("Wrong number of lines.", 2, c.getDocument().getNumberOfLines());
+			c.verifyContentByLine("", 0).verifyContentByLine("", 1);
+			c.writeAndVerify("output").writeFast("\f").write("more");
+			c.verifyContentByLine("output", 1);
+			c.verifyContentByLine("      more", 2);
+			c.clear();
+			c.writeFast("\f\f").writeFast("\f", err).write("\fend").verifyPartitions(2);
+			assertEquals("Wrong number of lines.", 5, c.getDocument().getNumberOfLines());
+			c.verifyContentByLine("end", 4);
+			c.clear();
+			c.write("1st\f2nd\f3rd").verifyPartitions();
+			c.verifyContentByLine("1st", 0);
+			c.verifyContentByLine("   2nd", 1);
+			c.verifyContentByLine("      3rd", 2);
+
+			// test form feed mixed with backspaces
+			c.clear();
+			c.write("first\f\b\bsecond");
+			c.verifyContentByLine("first", 0);
+			c.verifyContentByLine("   second", 1);
+			c.clear();
+			c.writeFast("><\b").writeFast("\f", err).write("abc").verifyPartitions(2);
+			c.verifyContentByLine("><", 0);
+			c.verifyContentByLine(" abc", 1);
+
+			// test with input partitions. At the moment input is
+			// considered for the indentation
+			c.clear();
+			c.writeAndVerify("foo").insertTyping("input").writeFast("bar").write("\f.", err).verifyPartitions(2);
+			c.verifyContentByLine("fooinputbar", 0);
+			c.verifyContentByLine("           .", 1);
+		}
+		closeConsole(c);
+	}
+
+	/**
 	 * Test larger number of partitions with pseudo random console content.
 	 */
 	public void testManyPartitions() throws IOException {
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
index dd7859b..d6a73d3 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
@@ -17,6 +17,7 @@
  *                          Bug 550621: Implementation of IConsoleDocumentPartitionerExtension
  *                          Bug 76936:  Support interpretation of \b and \r in console output
  *                          Bug 365770: Race condition in console clearing
+ *                          Bug 553282: Support interpretation of \f and \v in console output
  *******************************************************************************/
 package org.eclipse.ui.internal.console;
 
@@ -97,12 +98,12 @@
 	 * Pattern used to find supported ASCII control characters <b>except</b>
 	 * carriage return.
 	 */
-	private static final String CONTROL_CHARACTERS_PATTERN_STR = "(?:\b+)"; //$NON-NLS-1$
+	private static final String CONTROL_CHARACTERS_PATTERN_STR = "(?:\b+|\u000b+|\f+)"; //$NON-NLS-1$
 	/**
 	 * Pattern used to find supported ASCII control characters <b>including</b>
 	 * carriage return.
 	 */
-	private static final String CONTROL_CHARACTERS_WITH_CR_PATTERN_STR = "(?:\b+|\r+(?!\n))"; //$NON-NLS-1$
+	private static final String CONTROL_CHARACTERS_WITH_CR_PATTERN_STR = "(?:\b+|\u000b+|\f+|\r+(?!\n))"; //$NON-NLS-1$
 
 	/** The connected {@link IDocument} this partitioner manages. */
 	private IDocument document;
@@ -879,11 +880,11 @@
 
 						final String controlCharacterMatch = controlCharacterMatcher.group();
 						final char controlCharacter = controlCharacterMatch.charAt(0);
+						final int outputLineStartOffset = findOutputLineStartOffset(outputOffset);
 						switch (controlCharacter) {
 						case '\b':
 							// move virtual output cursor one step back for each \b
 							// but stop at current line start and skip any input partitions
-							final int outputLineStartOffset = findOutputLineStartOffset(outputOffset);
 							int backStepCount = controlCharacterMatch.length();
 							if (partitions.size() == 0) {
 								outputOffset = 0;
@@ -914,13 +915,34 @@
 								atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
 							}
 							outputOffset = Math.max(outputOffset, outputLineStartOffset);
+							nextWriteOffset = outputOffset;
 							break;
 
 						case '\r':
 							// move virtual output cursor to start of output line
-							outputOffset = findOutputLineStartOffset(outputOffset);
+							outputOffset = outputLineStartOffset;
 							atOutputPartitionIndex = -1;
 							atOutputPartition = null;
+							nextWriteOffset = outputOffset;
+							break;
+
+						case '\f':
+						case '\u000b': // \v
+							// Vertical tab does not override existing content. It will introduce a newline
+							// (at the end of current line even if output offset is inside the line) and
+							// indent the new line dependent on current output offset.
+							int indention = outputOffset - outputLineStartOffset;
+							final int vtabCount = controlCharacterMatch.length();
+							final StringBuilder vtab = new StringBuilder(indention + vtabCount);
+							for (int i = 0; i < vtabCount; i++) {
+								vtab.append(System.lineSeparator());
+							}
+							for (int i = 0; i < indention; i++) {
+								vtab.append(' ');
+							}
+							outputOffset = document.getLength();
+							nextWriteOffset = outputOffset;
+							partititonContent(pending.stream, vtab, 0, vtab.length());
 							break;
 
 						default:
@@ -929,12 +951,12 @@
 									+ Integer.toHexString(controlCharacter));
 							break;
 						}
-						nextWriteOffset = outputOffset;
 						textOffset = controlCharacterMatcher.end();
 					}
 				}
 			}
 			applyOutputToDocument(content.toString(), nextWriteOffset, replaceLength);
+			content = null;
 		}
 
 		/**