From 2048c0e37525189f7ae0760944e56d90339758e6 Mon Sep 17 00:00:00 2001 From: Paul Pazderski Date: Wed, 27 Mar 2019 13:40:00 +0100 Subject: Bug 76936 - [console] Handle \b and \r in console output Change-Id: Ia5e5b2760a0a9a097c003c41e3229e149003f22b Signed-off-by: Paul Pazderski --- .../debug/tests/console/IOConsoleTests.java | 154 ++++++++ .../internal/ui/DebugUIPreferenceInitializer.java | 4 +- .../ui/preferences/ConsolePreferencePage.java | 23 ++ .../ui/preferences/DebugPreferencesMessages.java | 4 +- .../DebugPreferencesMessages.properties | 4 +- .../ui/preferences/IDebugPreferenceConstants.java | 26 +- .../internal/ui/views/console/ProcessConsole.java | 7 + .../src/org/eclipse/ui/console/IOConsole.java | 55 +++ .../ui/internal/console/IOConsolePartitioner.java | 433 ++++++++++++++++++++- 9 files changed, 692 insertions(+), 18 deletions(-) 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 e244d2fbb..eb0cb0aa6 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 @@ -381,6 +381,160 @@ public class IOConsoleTests extends AbstractDebugTest { closeConsole(c, expectedInput.toArray(new String[0])); } + /** + * 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 \b. + */ + 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 \r. + */ + 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. */ 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 955bda6f4..3fb4c5069 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 @@ public class DebugUIPreferenceInitializer extends AbstractPreferenceInitializer 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 881290b8e..7789876a4 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 @@ public class ConsolePreferencePage extends FieldEditorPreferencePage implements private ConsoleIntegerFieldEditor fTabSizeEditor; private BooleanFieldEditor autoScrollLockEditor; + private BooleanFieldEditor2 fInterpretControlCharactersEditor; + private BooleanFieldEditor2 fInterpretCrAsControlCharacterEditor; + /** * Create the console page. */ @@ -157,6 +160,15 @@ public class ConsolePreferencePage extends FieldEditorPreferencePage implements 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 @@ public class ConsolePreferencePage extends FieldEditorPreferencePage implements updateWidthEditor(); updateAutoScrollLockEditor(); updateBufferSizeEditor(); + updateInterpretCrAsControlCharacterEditor(); } /** @@ -215,6 +228,15 @@ public class ConsolePreferencePage extends FieldEditorPreferencePage implements fBufferSizeEditor.getLabelControl(getFieldEditorParent()).setEnabled(b.getSelection()); } + /** + * 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() */ @@ -223,6 +245,7 @@ public class ConsolePreferencePage extends FieldEditorPreferencePage implements 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 41b530612..b74176979 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 class DebugPreferencesMessages extends NLS { 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 36ddf277d..c4cabbc6c 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_console_width=Character width must be between 80 and 1000 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 93d9c16ac..9b9355f8c 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 @@ public interface IDebugPreferenceConstants { */ String CONSOLE_TAB_WIDTH= "Console.console_tab_width"; //$NON-NLS-1$ + /** + * (boolean) If true console will interpret ASCII control + * characters like \b received from stdout or stderr (or any other + * connected output stream). + *

+ * If false 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. + *

+ */ + String CONSOLE_INTERPRET_CONTROL_CHARACTERS = "Console.interpret_control_characters"; //$NON-NLS-1$ + /** + * (boolean) Only used if {@link #CONSOLE_INTERPRET_CONTROL_CHARACTERS} is + * true. + *

+ * If true carriage returns are handled with there usual control + * character interpretation. (move output cursor to begin of line) + *

+ *

+ * If false carriage returns are not handled special and may result + * in line breaks since they are usually legal line delimiter. + *

+ */ + 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 bfb739aab..4f510b26e 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 @@ public class ProcessConsole extends IOConsole implements IConsole, IDebugEventSe 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 @@ public class ProcessConsole extends IOConsole implements IConsole, IDebugEventSe 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 77e6172fb..843717248 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 @@ public class IOConsole extends TextConsole { return this.charset; } + /** + * Check if console currently interprets ASCII control characters. + * + * @return true 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 + * (\b). + * + * @param handleControlCharacters interpret control characters if + * true + * @since 3.9 + */ + public void setHandleControlCharacters(boolean handleControlCharacters) { + partitioner.setHandleControlCharacters(handleControlCharacters); + } + + /** + * Check if carriage returns (\r) in console output are interpreted + * as control characters. They are also not interpreted if general control + * character handling is disabled. + * + * @return if true 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 (\r) are either ignored (false) + * and usually handled as line break by connected console document or if + * true interpreted with there control character meaning. + *

+ * Note: this option has no effect if control character interpretation is + * disabled in general. + *

+ * + * @param carriageReturnAsControlCharacter set false 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 477e7a16b..d2f6fbc4b 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.Comparator; 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 @@ public class IOConsolePartitioner */ private static final Comparator CMP_REGION_BY_OFFSET = Comparator.comparing(IRegion::getOffset); + /** + * Pattern used to find supported ASCII control characters except + * carriage return. + */ + private static final String CONTROL_CHARACTERS_PATTERN_STR = "(?:\b+)"; //$NON-NLS-1$ + /** + * Pattern used to find supported ASCII control characters including + * 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 @@ public class IOConsolePartitioner * 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 @@ public class IOConsolePartitioner /** Set after console signaled that all streams are closed. */ private volatile boolean streamsClosed; + /** + * Active pattern to search for supported control characters. If + * null control characters are treated as any other characters. + */ + private Pattern controlCharacterPattern = null; + /** + * Whether \r is interpreted as control characters + * (true) or not in console output. If false 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 @@ public class IOConsolePartitioner synchronized (partitions) { partitions.clear(); inputPartitions.clear(); + outputOffset = 0; } return new Region(0, 0); } @@ -428,6 +462,12 @@ public class IOConsolePartitioner 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 @@ public class IOConsolePartitioner 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); + } + 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. + *

+ * This method is also responsible to interpret control characters if enabled + * (see {@link #isHandleControlCharacters()}). + *

+ * + * @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 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); + } + } } - final int pendingLength = pendingPartition.text.length(); - lastPartition.setLength(lastPartition.getLength() + pendingLength); - nextOffset += pendingLength; - addedContent.append(pendingPartition.text); + // 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 @@ public class IOConsolePartitioner p.setOffset(offset); offset += p.getLength(); } + + // fix output offset + int removedLength = cutOffset; + outputOffset = Math.max(outputOffset - removedLength, 0); } if (ASSERT) { checkPartitions(); @@ -948,6 +1289,70 @@ public class IOConsolePartitioner return document != null ? document.getLength() : 0; } + /** + * Check if console currently interprets ASCII control characters. + * + * @return true 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 + * (\b). + * + * @param handleControlCharacters interpret control characters if + * true + * @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 (\r) are interpreted as control + * characters. They are also not interpreted if general control character + * handling is disabled. + * + * @return if true 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 + * (\r) are either ignored (false) and usually handled + * as line break by connected console document or if true + * interpreted with there control character meaning. + *

+ * Note: this option has no effect if control character interpretation is + * disabled in general. + *

+ * + * @param carriageReturnAsControlCharacter set false 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. * -- cgit v1.2.3