Bug 76936 - [console] Handle \b and \r in console output

Change-Id: Ia5e5b2760a0a9a097c003c41e3229e149003f22b
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
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 e244d2f..eb0cb0a 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
@@ -382,6 +382,160 @@
 	}
 
 	/**
+	 * Test enabling/disabling control character interpretation.
+	 */
+	public void testControlCharacterSettings() throws Exception {
+		final IOConsoleTestUtil c = getTestUtil("Test options");
+
+		c.getConsole().setHandleControlCharacters(false);
+		c.getConsole().setCarriageReturnAsControlCharacter(false);
+		c.write("\r..");
+		assertEquals("Wrong number of lines.", 2, c.getDocument().getNumberOfLines());
+
+		c.getConsole().setCarriageReturnAsControlCharacter(true);
+		c.write("\r..");
+		assertEquals("Wrong number of lines.", 3, c.getDocument().getNumberOfLines());
+
+		c.getConsole().setHandleControlCharacters(true);
+		c.getConsole().setCarriageReturnAsControlCharacter(false);
+		c.write("\r..");
+		assertEquals("Wrong number of lines.", 4, c.getDocument().getNumberOfLines());
+
+		c.getConsole().setCarriageReturnAsControlCharacter(true);
+		c.write("\r..");
+		assertEquals("Wrong number of lines.", 4, c.getDocument().getNumberOfLines());
+
+		closeConsole(c);
+		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+	}
+
+	/**
+	 * Test handling of <code>\b</code>.
+	 */
+	public void testBackspaceControlCharacter() throws Exception {
+		final IOConsoleTestUtil c = getTestUtil("Test \\b");
+		c.getConsole().setCarriageReturnAsControlCharacter(false);
+		c.getConsole().setHandleControlCharacters(true);
+		try (IOConsoleOutputStream err = c.getConsole().newOutputStream()) {
+			// test simple backspace cases
+			c.write("\b").write("|").verifyContent("|").verifyPartitions();
+			c.writeFast("\b").write("/").verifyContent("/").verifyPartitions();
+			c.writeFast("\b\b\b").write("-\b").verifyContent("-").verifyPartitions();
+			c.writeFast("\b1\b2\b3\b").write("\\").verifyContent("\\").verifyPartitions();
+
+			// test existing output is overwritten independent from stream
+			c.clear();
+			c.writeFast("out").write("err", err).verifyContent("outerr").verifyPartitions(2);
+			c.writeFast("\b\b\b\b\b\b\b\b\b\b\b\b\b");
+			c.writeFast("err", err).write("out").verifyContent("errout").verifyPartitions(2);
+			c.writeFast("\b\b\b\b\b\b\b\b\b\b\b\b\b");
+			c.writeFast("12", err).writeFast("345").write("6789", err).verifyContent("123456789").verifyPartitions(3);
+
+			// test backspace stops at line start
+			c.clear();
+			c.writeFast("First line\n").writeFast("\b\b", err).writeFast("Zecond line");
+			c.writeFast("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
+			c.write("S", err).verifyContentByLine("First line", 0).verifyContentByLine("Second line", 1).verifyPartitions(2);
+
+			// test in combination with input partitions
+			c.clear();
+			c.writeAndVerify("out").insertTyping("input").writeAndVerify("err", err).verifyContent("outinputerr").verifyPartitions(3);
+			c.setCaretOffset(6).backspace().backspace().writeAndVerify("~~~").verifyContentByOffset("~~~", -3).verifyPartitions(3);
+			c.verifyContent("outiuterr~~~");
+			c.writeFast("\b\b\b\b\b\b\b\b\b\b\b\b\b");
+			c.write("output").verifyContent("outiutput~~~").verifyPartitions(3);
+			c.setCaretOffset(4).insertTyping("np").verifyContent("outinputput~~~").verifyPartitions(3);
+			c.write("+++++", err).verifyContent("outinputput+++++").verifyPartitions(3);
+			c.writeFast(String.join("", Collections.nCopies(11, "\b")));
+			c.write("err", err).verifyContent("errinputput+++++").verifyPartitions(3);
+
+			c.clear();
+			c.writeAndVerify("ooooo").insertTyping("iii").write("eeee", err).moveCaretToEnd().insertTyping("i").write("oo");
+			c.verifyContent("oooooiiieeeeioo").verifyPartitions(3);
+			c.writeFast(String.join("", Collections.nCopies(7, "\b")));
+			c.write("xx").verifyContent("ooooxiiixeeeioo").verifyPartitions(3);
+
+			c.clear();
+			c.insert("iiii").writeFast("\b").write("o").verifyContent("iiiio").verifyPartitions(2);
+			c.write("\b\bee", err).verifyContentByOffset("iiiiee", 0).verifyPartitions(2);
+			c.writeFast("\b\b\b\b\b\b\b\b", err).write("o").verifyContent("iiiioe").verifyPartitions(3);
+
+			// test if backspace overruns line breaks introduced by input
+			// (at the moment it should overrun those line breaks)
+			c.clear();
+			c.writeAndVerify("1", err).insertTyping("input").enter().write("2");
+			c.verifyContentByLine("1input", 0).verifyContentByLine("2", 1).verifyPartitions(3);
+			c.writeFast("\b\b\b\b\b\b\b\b\b\b\b\b\b", err);
+			c.write("???").verifyContentByLine("?input", 0).verifyContentByLine("??", 1).verifyPartitions(3);
+			c.writeFast("\b\b").writeFast("\b", err).write("><~");
+			c.verifyContentByLine(">input", 0).verifyContentByLine("<~", 1).verifyPartitions(3);
+
+			// test output cursor moves according to changed input
+			c.clear();
+			c.writeAndVerify("abc", err).insert("<>").write("def").verifyContent("abc<>def").verifyPartitions(3);
+			c.write("\b\b").setCaretOffset(4).insertTypingAndVerify("-=-").verifyContent("abc<-=->def").verifyPartitions(3);
+			c.moveCaret(-1).backspace().verifyContent("abc<-->def").verifyPartitions(3);
+			c.write("e\b\b\b\b", err).insertTyping("++").verifyContent("abc<-++->def").verifyPartitions(3);
+			c.select(0, c.getDocument().getLength()).backspace().write("b").verifyContent("abcdef").verifyPartitions(3);
+
+			// break output line
+			// NOTE: this may not be the desired behavior
+			c.clear();
+			c.writeFast("1.2.").writeFast("\b\b").write("\n");
+			c.verifyContentByLine("1.", 0).verifyContentByLine(".", 1).verifyPartitions();
+			c.writeFast("\b\b\b\b").write("2.");
+			c.verifyContentByLine("1.", 0).verifyContentByLine("2.", 1).verifyPartitions();
+		}
+		closeConsole(c);
+		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+	}
+
+	/**
+	 * Test handling of <code>\r</code>.
+	 */
+	public void testCarriageReturnControlCharacter() throws Exception {
+		final IOConsoleTestUtil c = getTestUtil("Test \\r");
+		c.getConsole().setCarriageReturnAsControlCharacter(true);
+		c.getConsole().setHandleControlCharacters(true);
+		try (IOConsoleOutputStream err = c.getConsole().newOutputStream()) {
+			// test simple carriage return cases
+			c.write("\r");
+			assertEquals("Wrong number of lines.", 1, c.getDocument().getNumberOfLines());
+			c.writeFast("bad", err).write("\rgood").verifyContent("good").verifyPartitions(1);
+			assertEquals("Wrong number of lines.", 1, c.getDocument().getNumberOfLines());
+
+			// test carriage return stops at line start
+			c.clear();
+			c.writeFast("First line\r\n").write("Zecond line", err);
+			c.verifyContentByLine("First line", 0).verifyContentByLine("Zecond line", 1).verifyPartitions(2);
+			assertEquals("Wrong number of lines.", 2, c.getDocument().getNumberOfLines());
+			c.writeFast("\r").write("3.    ").verifyContentByLine("3.     line", 1).verifyPartitions(2);
+			assertEquals("Wrong number of lines.", 2, c.getDocument().getNumberOfLines());
+			c.writeFast("\r\r\r", err).write("Second").verifyContentByLine("Second line", 1).verifyPartitions(2);
+			assertEquals("Wrong number of lines.", 2, c.getDocument().getNumberOfLines());
+
+			// test carriage return with input partitions
+			c.clear();
+			c.insertTypingAndVerify("input").writeFast("out\r").write("err", err);
+			c.verifyContent("inputerr").verifyPartitions(2);
+			c.enter().write("\rout").verifyContentByLine("inputout", 0).verifyPartitions(2);
+			c.write("err", err).verifyContentByLine("err", 1).verifyPartitions(3);
+			c.write("\roooooo").verifyContentByLine("inputooo", 0).verifyContentByLine("ooo", 1).verifyPartitions(2);
+
+			// test in combination with \r\n
+			c.clear();
+			c.write("\r\n");
+			assertEquals("Wrong number of lines.", 2, c.getDocument().getNumberOfLines());
+			c.writeFast("err", err).writeFast("\r\r\r\r\r\r\r\r\n\n").write("out");
+			assertEquals("Wrong number of lines.", 4, c.getDocument().getNumberOfLines());
+			c.verifyContentByLine("out", -1).verifyPartitions();
+			assertTrue("Line breaks did not overwrite text.", !c.getDocument().get().contains("err"));
+		}
+		closeConsole(c);
+		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+	}
+
+	/**
 	 * Test larger number of partitions with pseudo random console content.
 	 */
 	public void testManyPartitions() throws IOException {
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java
index 955bda6..3fb4c50 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- *  Copyright (c) 2004, 2018 IBM Corporation and others.
+ *  Copyright (c) 2004, 2019 IBM Corporation and others.
  *
  *  This program and the accompanying materials
  *  are made available under the terms of the Eclipse Public License 2.0
@@ -80,6 +80,8 @@
 		prefs.setDefault(IDebugPreferenceConstants.CONSOLE_LOW_WATER_MARK, 80000);
 		prefs.setDefault(IDebugPreferenceConstants.CONSOLE_HIGH_WATER_MARK, 100000);
 		prefs.setDefault(IDebugPreferenceConstants.CONSOLE_TAB_WIDTH, 8);
+		prefs.setDefault(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS, false);
+		prefs.setDefault(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER, true);
 
 		// console colors
 		setThemeBasedPreferences(prefs, false);
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java
index 881290b..7789876 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java
@@ -81,6 +81,9 @@
 	private ConsoleIntegerFieldEditor fTabSizeEditor;
 	private BooleanFieldEditor autoScrollLockEditor;
 
+	private BooleanFieldEditor2 fInterpretControlCharactersEditor;
+	private BooleanFieldEditor2 fInterpretCrAsControlCharacterEditor;
+
 	/**
 	 * Create the console page.
 	 */
@@ -157,6 +160,15 @@
 		addField(syserr);
 		addField(sysin);
 		addField(background);
+
+		fInterpretControlCharactersEditor = new BooleanFieldEditor2(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS, DebugPreferencesMessages.ConsolePreferencePage_Interpret_control_characters, SWT.NONE, getFieldEditorParent());
+		fInterpretCrAsControlCharacterEditor = new BooleanFieldEditor2(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER, DebugPreferencesMessages.ConsolePreferencePage_Interpret_cr_as_control_character, SWT.NONE, getFieldEditorParent());
+
+		fInterpretControlCharactersEditor.getChangeControl(getFieldEditorParent()).addListener(SWT.Selection,
+				event -> updateInterpretCrAsControlCharacterEditor());
+
+		addField(fInterpretControlCharactersEditor);
+		addField(fInterpretCrAsControlCharacterEditor);
 	}
 
 	/**
@@ -186,6 +198,7 @@
 		updateWidthEditor();
 		updateAutoScrollLockEditor();
 		updateBufferSizeEditor();
+		updateInterpretCrAsControlCharacterEditor();
 	}
 
 	/**
@@ -216,6 +229,15 @@
 	}
 
 	/**
+	 * Update enablement of carriage return interpretation based on general control
+	 * character interpretation.
+	 */
+	protected void updateInterpretCrAsControlCharacterEditor() {
+		Button b = fInterpretControlCharactersEditor.getChangeControl(getFieldEditorParent());
+		fInterpretCrAsControlCharacterEditor.getChangeControl(getFieldEditorParent()).setEnabled(b.getSelection());
+	}
+
+	/**
 	 * @see org.eclipse.jface.preference.PreferencePage#performDefaults()
 	 */
 	@Override
@@ -223,6 +245,7 @@
 		super.performDefaults();
 		updateWidthEditor();
 		updateBufferSizeEditor();
+		updateInterpretCrAsControlCharacterEditor();
 	}
 
 	protected boolean canClearErrorMessage() {
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java
index 41b5306..b741769 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- *  Copyright (c) 2000, 2018 IBM Corporation and others.
+ *  Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  *  This program and the accompanying materials
  *  are made available under the terms of the Eclipse Public License 2.0
@@ -35,6 +35,8 @@
 	public static String ConsolePreferencePage_console_width;
 	public static String ConsolePreferencePage_12;
 	public static String ConsolePreferencePage_13;
+	public static String ConsolePreferencePage_Interpret_control_characters;
+	public static String ConsolePreferencePage_Interpret_cr_as_control_character;
 
 	public static String DebugPreferencePage_1;
 	public static String DebugPreferencePage_2;
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties
index 36ddf27..c4cabbc 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties
@@ -1,5 +1,5 @@
 ###############################################################################
-# Copyright (c) 2000, 2018 IBM Corporation and others.
+# Copyright (c) 2000, 2019 IBM Corporation and others.
 #
 # This program and the accompanying materials
 # are made available under the terms of the Eclipse Public License 2.0
@@ -28,6 +28,8 @@
 ConsolePreferencePage_12=Displayed &tab width:
 ConsolePreferencePage_13=Tab width must be between 1 and 100 inclusive.
 ConsolePreferencePage_11=Back&ground color:
+ConsolePreferencePage_Interpret_control_characters=Interpret ASCII &control characters
+ConsolePreferencePage_Interpret_cr_as_control_character=Interpret Carriage &Return (\\r) as control character 
 
 DebugPreferencePage_1=General Settings for Running and Debugging.
 DebugPreferencePage_2=Re&use editor when displaying source code
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java
index 93d9c16..9b9355f 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -91,7 +91,31 @@
 	 */
 	String CONSOLE_TAB_WIDTH= "Console.console_tab_width"; //$NON-NLS-1$
 
+	/**
+	 * (boolean) If <code>true</code> console will interpret ASCII control
+	 * characters like <code>\b</code> received from stdout or stderr (or any other
+	 * connected output stream).
+	 * <p>
+	 * If <code>false</code> control characters are appended to console like any
+	 * other character. Since they are usually not printable they may be invisible
+	 * or result in some Unicode default representation.
+	 * </p>
+	 */
+	String CONSOLE_INTERPRET_CONTROL_CHARACTERS = "Console.interpret_control_characters"; //$NON-NLS-1$
 
+	/**
+	 * (boolean) Only used if {@link #CONSOLE_INTERPRET_CONTROL_CHARACTERS} is
+	 * <code>true</code>.
+	 * <p>
+	 * If <code>true</code> carriage returns are handled with there usual control
+	 * character interpretation. (move output cursor to begin of line)
+	 * </p>
+	 * <p>
+	 * If <code>false</code> carriage returns are not handled special and may result
+	 * in line breaks since they are usually legal line delimiter.
+	 * </p>
+	 */
+	String CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER = "Console.interpret_cr_as_control_characters"; //$NON-NLS-1$
 
 	/**
 	 * The orientation of the detail view in the VariablesView
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java
index bfb739a..4f510b2 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java
@@ -376,6 +376,10 @@
 			setFont(JFaceResources.getFont(IDebugUIConstants.PREF_CONSOLE_FONT));
 		} else if (property.equals(IDebugPreferenceConstants.CONSOLE_BAKGROUND_COLOR)) {
 			setBackground(DebugUIPlugin.getPreferenceColor(IDebugPreferenceConstants.CONSOLE_BAKGROUND_COLOR));
+		} else if (property.equals(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS)) {
+			setHandleControlCharacters(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS));
+		} else if (property.equals(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER)) {
+			setCarriageReturnAsControlCharacter(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER));
 		}
 	}
 
@@ -485,6 +489,9 @@
 			setWaterMarks(lowWater, highWater);
 		}
 
+		setHandleControlCharacters(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS));
+		setCarriageReturnAsControlCharacter(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER));
+
 		DebugUIPlugin.getStandardDisplay().asyncExec(() -> {
 			setFont(JFaceResources.getFont(IDebugUIConstants.PREF_CONSOLE_FONT));
 			setBackground(DebugUIPlugin.getPreferenceColor(IDebugPreferenceConstants.CONSOLE_BAKGROUND_COLOR));
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java
index 77e6172..8437172 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java
@@ -347,4 +347,59 @@
 		return this.charset;
 	}
 
+	/**
+	 * Check if console currently interprets ASCII control characters.
+	 *
+	 * @return <code>true</code> if console interprets ASCII control characters
+	 * @since 3.9
+	 */
+	public boolean isHandleControlCharacters() {
+		return partitioner.isHandleControlCharacters();
+	}
+
+	/**
+	 * Enable or disable interpretation of ASCII control characters like backspace
+	 * (<code>\b</code>).
+	 *
+	 * @param handleControlCharacters interpret control characters if
+	 *                                <code>true</code>
+	 * @since 3.9
+	 */
+	public void setHandleControlCharacters(boolean handleControlCharacters) {
+		partitioner.setHandleControlCharacters(handleControlCharacters);
+	}
+
+	/**
+	 * Check if carriage returns (<code>\r</code>) in console output are interpreted
+	 * as control characters. They are also not interpreted if general control
+	 * character handling is disabled.
+	 *
+	 * @return if <code>true</code> carriage returns are interpreted as control
+	 *         characters.
+	 * @see #isHandleControlCharacters()
+	 * @since 3.9
+	 */
+	public boolean isCarriageReturnAsControlCharacter() {
+		return partitioner.isCarriageReturnAsControlCharacter();
+	}
+
+	/**
+	 * If control characters in console output are interpreted by this console
+	 * carriage returns (<code>\r</code>) are either ignored (<code>false</code>)
+	 * and usually handled as line break by connected console document or if
+	 * <code>true</code> interpreted with there control character meaning.
+	 * <p>
+	 * Note: this option has no effect if control character interpretation is
+	 * disabled in general.
+	 * </p>
+	 *
+	 * @param carriageReturnAsControlCharacter set <code>false</code> to exclude
+	 *                                         carriage return from control
+	 *                                         character interpretation
+	 * @see #setHandleControlCharacters(boolean)
+	 * @since 3.9
+	 */
+	public void setCarriageReturnAsControlCharacter(boolean carriageReturnAsControlCharacter) {
+		partitioner.setCarriageReturnAsControlCharacter(carriageReturnAsControlCharacter);
+	}
 }
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 477e7a1..d2f6fbc 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
@@ -15,6 +15,7 @@
  *                          Bug 548356: fixed user input handling
  *                          Bug 550618: getStyleRanges produced invalid overlapping styles
  *                          Bug 550621: Implementation of IConsoleDocumentPartitionerExtension
+ *                          Bug 76936:  Support interpretation of \b and \r in console output
  *******************************************************************************/
 package org.eclipse.ui.internal.console;
 
@@ -25,6 +26,8 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -89,6 +92,17 @@
 	 */
 	private static final Comparator<IRegion> CMP_REGION_BY_OFFSET = Comparator.comparing(IRegion::getOffset);
 
+	/**
+	 * 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$
+	/**
+	 * 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$
+
 	/** The connected {@link IDocument} this partitioner manages. */
 	private IDocument document;
 	/**
@@ -130,6 +144,10 @@
 	 * length trimming is scheduled. Trimming is disabled if value is negative.
 	 */
 	private int highWaterMark = -1;
+	/**
+	 * The low mark for console content trimming. If trim is performed approximate
+	 * this many characters are remain in console.
+	 */
 	private int lowWaterMark = -1;
 
 	/** The partitioned {@link IOConsole}. */
@@ -137,6 +155,21 @@
 
 	/** Set after console signaled that all streams are closed. */
 	private volatile boolean streamsClosed;
+	/**
+	 * Active pattern to search for supported control characters. If
+	 * <code>null</code> control characters are treated as any other characters.
+	 */
+	private Pattern controlCharacterPattern = null;
+	/**
+	 * Whether <code>\r</code> is interpreted as control characters
+	 * (<code>true</code>) or not in console output. If <code>false</code> they are
+	 * probably handled as newline.
+	 */
+	private boolean carriageReturnAsControlCharacter = true;
+	/**
+	 * Offset where next output is written to console.
+	 */
+	private int outputOffset = 0;
 
 	/**
 	 * Create new partitioner for an {@link IOConsole}.
@@ -421,6 +454,7 @@
 				synchronized (partitions) {
 					partitions.clear();
 					inputPartitions.clear();
+					outputOffset = 0;
 				}
 				return new Region(0, 0);
 			}
@@ -428,6 +462,12 @@
 			synchronized (partitions) {
 				switch (updateType) {
 				case INPUT:
+					if (event.getOffset() <= outputOffset) { // move output offset if necessary
+						outputOffset -= Math.min(event.getLength(), outputOffset - event.getOffset());
+						if (event.getText() != null) {
+							outputOffset += event.getText().length();
+						}
+					}
 					return applyUserInput(event);
 
 				// update and trim jobs are triggered by this partitioner and all partitioning
@@ -740,28 +780,325 @@
 			pendingPartitions.notifyAll();
 		}
 		synchronized (partitions) {
-			final StringBuilder addedContent = new StringBuilder(size);
-			IOConsolePartition lastPartition = getPartitionByIndex(partitions.size() - 1);
-			int nextOffset = document.getLength();
-			for (PendingPartition pendingPartition : pendingCopy) {
-				if (lastPartition == null || lastPartition.getOutputStream() != pendingPartition.stream) {
-					lastPartition = new IOConsolePartition(nextOffset, pendingPartition.stream);
-					partitions.add(lastPartition);
+			if (isHandleControlCharacters()) {
+				applyStreamOutput(pendingCopy, size);
+			} else {
+				// Old implementation of output appending. The control character aware variant
+				// {@link #applyStreamOutput(List, int)} should do exactly the same if control
+				// character processing is disabled but since there is not so much time for
+				// testing in current development cycle the old implementation is used to
+				// process output when control character interpretation is disabled.
+				// TODO remove in next development cycle
+				final StringBuilder addedContent = new StringBuilder(size);
+				IOConsolePartition lastPartition = getPartitionByIndex(partitions.size() - 1);
+				int nextOffset = document.getLength();
+				for (PendingPartition pendingPartition : pendingCopy) {
+					if (lastPartition == null || lastPartition.getOutputStream() != pendingPartition.stream) {
+						lastPartition = new IOConsolePartition(nextOffset, pendingPartition.stream);
+						partitions.add(lastPartition);
+					}
+					final int pendingLength = pendingPartition.text.length();
+					lastPartition.setLength(lastPartition.getLength() + pendingLength);
+					nextOffset += pendingLength;
+					addedContent.append(pendingPartition.text);
 				}
-				final int pendingLength = pendingPartition.text.length();
-				lastPartition.setLength(lastPartition.getLength() + pendingLength);
-				nextOffset += pendingLength;
-				addedContent.append(pendingPartition.text);
+				try {
+					updateType = DocUpdateType.OUTPUT;
+					document.replace(document.getLength(), 0, addedContent.toString());
+					outputOffset += addedContent.length();
+				} catch (BadLocationException e) {
+					log(e);
+				}
+			}
+		}
+		checkFinished();
+		checkBufferSize();
+	}
+
+	/**
+	 * Apply content collected in pending partitions to document and update
+	 * partitioning structure.
+	 * <p>
+	 * This method is also responsible to interpret control characters if enabled
+	 * (see {@link #isHandleControlCharacters()}).
+	 * </p>
+	 *
+	 * @param pendingCopy the pending partitions to process
+	 * @param sizeHint    a hint for expected content length to initialize buffer
+	 *                    size. Does not have to be exact as long as it is not
+	 *                    negative.
+	 */
+	private void applyStreamOutput(List<PendingPartition> pendingCopy, int sizeHint) {
+		// local reference to get consistent parsing without blocking pattern changes
+		final Pattern controlPattern = controlCharacterPattern;
+		// Variables to collect required data to reduce number of document updates. The
+		// partitioning must be updated in smaller iterations as the actual document
+		// content. E.g. pending partitions are distinct on source output stream
+		// resulting in multiple partitions but if all the content is appended to the
+		// document there is only one update required to add the actual content.
+		int nextWriteOffset = outputOffset;
+		final StringBuilder content = new StringBuilder(sizeHint);
+		int replaceLength = 0;
+		// the partition which contains the current output offset
+		IOConsolePartition atOutputPartition = null;
+		// the index of atOutputPartition in the partitions list
+		int atOutputPartitionIndex = -1;
+
+		for (PendingPartition pending : pendingCopy) {
+			// create matcher to find control characters in pending content (if enabled)
+			final Matcher controlCharacterMatcher = controlPattern != null ? controlPattern.matcher(pending.text)
+					: null;
+
+			for (int textOffset = 0; textOffset < pending.text.length();) {
+				// Process pending content in chunks.
+				// Processing is primary split on control characters since there interpretation
+				// is easier if all content changes before are already applied.
+				// Additional processing splits may result while overwriting existing output and
+				// overwrite overlaps partitions.
+				final boolean foundControlCharacter;
+				final int partEnd;
+				if (controlCharacterMatcher != null && controlCharacterMatcher.find()) {
+					if (ASSERT) {
+						// check used pattern. Assert it matches only sequences of same characters.
+						final String match = controlCharacterMatcher.group();
+						Assert.isTrue(match.length() > 0);
+						final char matchedChar = match.charAt(0);
+						for (char c : match.toCharArray()) {
+							Assert.isTrue(c == matchedChar);
+						}
+					}
+					partEnd = controlCharacterMatcher.start();
+					foundControlCharacter = true;
+				} else {
+					partEnd = pending.text.length();
+					foundControlCharacter = false;
+				}
+
+				while (textOffset < partEnd) {
+					// Process content part. This part never contains control characters.
+					// Processing may require multiple iterations if we overwrite existing content
+					// which consists of distinct partitions.
+
+					if (outputOffset >= document.getLength()) {
+						// content is appended to document end (the easy case)
+						if (atOutputPartition == null) {
+							// get the last existing partition to try to expand it
+							atOutputPartitionIndex = partitions.size() - 1;
+							atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+							if (ASSERT) {
+								Assert.isTrue(atOutputPartitionIndex == findPartitionCandidate(outputOffset - 1));
+							}
+						}
+						if (atOutputPartition == null || atOutputPartition.getOutputStream() != pending.stream) {
+							// no partitions yet or last partition is incompatible to reuse -> add new one
+							atOutputPartition = new IOConsolePartition(outputOffset, pending.stream);
+							partitions.add(atOutputPartition);
+							atOutputPartitionIndex = partitions.size() - 1;
+						}
+						final int appendedLength = partEnd - textOffset;
+						content.append(pending.text, textOffset, partEnd);
+						atOutputPartition.setLength(atOutputPartition.getLength() + appendedLength);
+						outputOffset += appendedLength;
+						textOffset = partEnd;
+					} else {
+						// content overwrites existing console content (the tricky case)
+						if (atOutputPartition == null) {
+							// find partition where output will overwrite or create one if unpartitioned
+							atOutputPartitionIndex = findPartitionCandidate(outputOffset);
+							atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+							if (atOutputPartition == null) {
+								atOutputPartition = new IOConsolePartition(outputOffset, pending.stream);
+								atOutputPartitionIndex++;
+								partitions.add(atOutputPartitionIndex, atOutputPartition);
+							}
+						}
+
+						// we do not overwrite input partitions at the moment so they need to be skipped
+						if (isInputPartition(atOutputPartition)) {
+							outputOffset = atOutputPartition.getOffset() + atOutputPartition.getLength();
+							atOutputPartitionIndex++;
+							atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+
+							// apply document changes collected until now
+							applyOutputToDocument(content.toString(), nextWriteOffset, replaceLength);
+							content.setLength(0);
+							replaceLength = 0;
+							nextWriteOffset = outputOffset;
+							continue; // to check if next selected partition is also input or appending now
+						}
+
+						// limit chunks to overwrite only one existing partition at a time
+						final int chunkLength = Math.min(partEnd - textOffset,
+								atOutputPartition.getLength() - (outputOffset - atOutputPartition.getOffset()));
+						Assert.isTrue(chunkLength > 0); // do not remove since it can prevent an infinity loop
+
+						if (atOutputPartition.getOutputStream() != pending.stream) {
+							// new output is from other stream then overwritten output
+
+							// Note: this implementation ignores the possibility to reuse the partition
+							// where the overwrite chunk ends and expand it towards replace begin since this
+							// makes things code much more complex. In some cases this may leads to
+							// consecutive partitions which could be merged to one partition. Merging is not
+							// implemented at the moment.
+
+							// in this part outputPartition is used to partition the new content
+							// and atOutputPartition points to the partition whose content is overwritten
+							// i.e. the new partition grows and the old one must shrink
+							IOConsolePartition outputPartition = null;
+							if (atOutputPartition.getOffset() == outputOffset) {
+								// try to expand the partition before our output offset
+								outputPartition = getPartitionByIndex(atOutputPartitionIndex - 1);
+							} else {
+								// overwrite starts inside existing incompatible partition
+								atOutputPartition = splitPartition(outputOffset);
+								atOutputPartitionIndex++;
+							}
+							if (outputPartition == null || outputPartition.getOutputStream() != pending.stream) {
+								outputPartition = new IOConsolePartition(outputOffset, pending.stream);
+								partitions.add(atOutputPartitionIndex, outputPartition);
+								atOutputPartitionIndex++;
+							}
+
+							// update partitioning of the overwritten chunk
+							outputPartition.setLength(outputPartition.getLength() + chunkLength);
+							atOutputPartition.setOffset(atOutputPartition.getOffset() + chunkLength);
+							atOutputPartition.setLength(atOutputPartition.getLength() - chunkLength);
+
+							if (atOutputPartition.getLength() == 0) {
+								// overwritten partition is now empty and must be be removed
+								partitions.remove(atOutputPartitionIndex);
+								atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+							}
+						}
+						content.append(pending.text, textOffset, textOffset + chunkLength);
+						replaceLength += chunkLength;
+						textOffset += chunkLength;
+						outputOffset += chunkLength;
+						if (atOutputPartition != null
+								&& outputOffset == atOutputPartition.getOffset() + atOutputPartition.getLength()) {
+							atOutputPartitionIndex++;
+							atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+						}
+					}
+				}
+				// finished processing of regular content before control characters
+				// now interpret control characters if any
+				if (controlCharacterMatcher != null && foundControlCharacter) {
+					// at first update console document since it is easier to interpret control
+					// characters on an up-to-date document and partitioning
+					applyOutputToDocument(content.toString(), nextWriteOffset, replaceLength);
+					content.setLength(0);
+					replaceLength = 0;
+
+					final String controlCharacterMatch = controlCharacterMatcher.group();
+					final char controlCharacter = controlCharacterMatch.charAt(0);
+					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;
+							break;
+						}
+						if (atOutputPartition == null) {
+							atOutputPartitionIndex = partitions.size() - 1;
+							atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+						}
+						while (backStepCount > 0 && outputOffset > outputLineStartOffset) {
+							if (atOutputPartition != null && isInputPartition(atOutputPartition)) {
+								do {
+									outputOffset = atOutputPartition.getOffset() - 1;
+									atOutputPartitionIndex--;
+									atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+								} while (atOutputPartition != null && isInputPartition(atOutputPartition));
+								backStepCount--;
+							}
+							if (atOutputPartition == null) {
+								outputOffset = 0;
+								break;
+							}
+							final int backSteps = Math.min(outputOffset - atOutputPartition.getOffset(), backStepCount);
+							outputOffset -= backSteps;
+							backStepCount -= backSteps;
+							atOutputPartitionIndex--;
+							atOutputPartition = getPartitionByIndex(atOutputPartitionIndex);
+						}
+						outputOffset = Math.max(outputOffset, outputLineStartOffset);
+						break;
+
+					case '\r':
+						// move virtual output cursor to start of output line
+						outputOffset = findOutputLineStartOffset(outputOffset);
+						atOutputPartitionIndex = -1;
+						atOutputPartition = null;
+						break;
+
+					default:
+						// should never happen as long as the used regex pattern is valid
+						log(IStatus.ERROR, "No implementation to handle control character 0x" //$NON-NLS-1$
+								+ Integer.toHexString(controlCharacter));
+						break;
+					}
+					nextWriteOffset = outputOffset;
+					textOffset = controlCharacterMatcher.end();
+				}
+			}
+		}
+		applyOutputToDocument(content.toString(), nextWriteOffset, replaceLength);
+	}
+
+	/**
+	 * Find offset of line start from given output offset. This method ignores line
+	 * breaks partitioned as input. I.e. it looks at the document as if it only
+	 * consist of the output parts.
+	 *
+	 * @param outOffset offset where output should be written
+	 * @return the start offset of line where output should be written
+	 */
+	private int findOutputLineStartOffset(int outOffset) {
+		int outputLineStartOffset = 0;
+		try {
+			for (int lineIndex = document.getLineOfOffset(outOffset); lineIndex >= 0; lineIndex--) {
+				outputLineStartOffset = document.getLineOffset(lineIndex);
+				final IOConsolePartition lineBreakPartition = getIOPartition(outputLineStartOffset - 1);
+				if (lineBreakPartition == null || !isInputPartition(lineBreakPartition)) {
+					break;
+				}
+			}
+		} catch (BadLocationException e) {
+			log(e);
+			outputLineStartOffset = 0;
+		}
+		if (ASSERT) {
+			Assert.isTrue(outputLineStartOffset <= outOffset);
+		}
+		return outputLineStartOffset;
+	}
+
+	/**
+	 * Apply content from output streams to document. It expects the partitioning
+	 * has or will update partitioning to reflect the change since it prevents this
+	 * partitioner's {@link #documentChanged2(DocumentEvent)} method from changing
+	 * partitioning.
+	 *
+	 * @param content       collected content from output streams
+	 * @param offset        offset where content is inserted
+	 * @param replaceLength length of overwritten old output
+	 */
+	private void applyOutputToDocument(String content, int offset, int replaceLength) {
+		if (content.length() > 0 || replaceLength > 0) {
+			if (ASSERT) {
+				Assert.isTrue(replaceLength <= content.length());
 			}
 			try {
 				updateType = DocUpdateType.OUTPUT;
-				document.replace(document.getLength(), 0, addedContent.toString());
+				document.replace(offset, replaceLength, content);
 			} catch (BadLocationException e) {
 				log(e);
 			}
 		}
-		checkBufferSize();
-		checkFinished();
 	}
 
 	/**
@@ -828,6 +1165,10 @@
 								p.setOffset(offset);
 								offset += p.getLength();
 							}
+
+							// fix output offset
+							int removedLength = cutOffset;
+							outputOffset = Math.max(outputOffset - removedLength, 0);
 						}
 						if (ASSERT) {
 							checkPartitions();
@@ -949,6 +1290,70 @@
 	}
 
 	/**
+	 * Check if console currently interprets ASCII control characters.
+	 *
+	 * @return <code>true</code> if console interprets ASCII control characters
+	 * @since 3.9
+	 */
+	public boolean isHandleControlCharacters() {
+		return controlCharacterPattern != null;
+	}
+
+	/**
+	 * Enable or disable interpretation of ASCII control characters like backspace
+	 * (<code>\b</code>).
+	 *
+	 * @param handleControlCharacters interpret control characters if
+	 *                                <code>true</code>
+	 * @since 3.9
+	 */
+	public void setHandleControlCharacters(boolean handleControlCharacters) {
+		if (handleControlCharacters) {
+			controlCharacterPattern = Pattern
+					.compile(carriageReturnAsControlCharacter ? CONTROL_CHARACTERS_WITH_CR_PATTERN_STR
+							: CONTROL_CHARACTERS_PATTERN_STR);
+		} else {
+			controlCharacterPattern = null;
+		}
+	}
+
+	/**
+	 * Check if carriage returns (<code>\r</code>) are interpreted as control
+	 * characters. They are also not interpreted if general control character
+	 * handling is disabled.
+	 *
+	 * @return if <code>true</code> carriage returns are interpreted as control
+	 *         characters.
+	 * @see #isHandleControlCharacters()
+	 * @since 3.9
+	 */
+	public boolean isCarriageReturnAsControlCharacter() {
+		return carriageReturnAsControlCharacter;
+	}
+
+	/**
+	 * If control characters are interpreted by this console carriage returns
+	 * (<code>\r</code>) are either ignored (<code>false</code>) and usually handled
+	 * as line break by connected console document or if <code>true</code>
+	 * interpreted with there control character meaning.
+	 * <p>
+	 * Note: this option has no effect if control character interpretation is
+	 * disabled in general.
+	 * </p>
+	 *
+	 * @param carriageReturnAsControlCharacter set <code>false</code> to exclude
+	 *                                         carriage return from control
+	 *                                         character interpretation
+	 * @see #setHandleControlCharacters(boolean)
+	 * @since 3.9
+	 */
+	public void setCarriageReturnAsControlCharacter(boolean carriageReturnAsControlCharacter) {
+		this.carriageReturnAsControlCharacter = carriageReturnAsControlCharacter;
+		// reset to update control character pattern
+		setHandleControlCharacters(isHandleControlCharacters());
+	}
+
+	/**
 	 * Get a partition by its index. Safe from out of bounds exceptions.
 	 *
 	 * @param index index of requested partition