diff --git a/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF b/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF index 7016e231f40..1dd65e70878 100644 --- a/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF +++ b/debug/org.eclipse.debug.tests/META-INF/MANIFEST.MF @@ -31,10 +31,11 @@ Export-Package: org.eclipse.debug.tests, Import-Package: org.assertj.core.api;version="3.24.2", org.assertj.core.api.iterable, org.junit.jupiter.api;version="[5.14.0,6.0.0)", - org.junit.jupiter.api.io;version="[5.14.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)", org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", + org.junit.jupiter.api.io;version="[5.14.0,6.0.0)", org.junit.jupiter.params;version="[5.14.0,6.0.0)", + org.junit.jupiter.params.provider;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)", org.opentest4j;version="[1.3.0,2.0.0)" Eclipse-BundleShape: dir diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java index 52be8447f02..efd2349e0c8 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java @@ -19,8 +19,10 @@ import org.eclipse.debug.tests.breakpoint.BreakpointTests; import org.eclipse.debug.tests.breakpoint.SerialExecutorTest; import org.eclipse.debug.tests.console.ConsoleDocumentAdapterTests; -import org.eclipse.debug.tests.console.ConsoleShowHideTests; import org.eclipse.debug.tests.console.ConsoleManagerTests; +import org.eclipse.debug.tests.console.ConsoleOutputLineTruncateTest; +import org.eclipse.debug.tests.console.ConsoleOutputLineWrapTest; +import org.eclipse.debug.tests.console.ConsoleShowHideTests; import org.eclipse.debug.tests.console.ConsoleTests; import org.eclipse.debug.tests.console.FileLinkTests; import org.eclipse.debug.tests.console.IOConsoleFixedWidthTests; @@ -122,6 +124,8 @@ ConsoleManagerTests.class, // ConsoleTests.class, // IOConsoleTests.class, // + ConsoleOutputLineWrapTest.class, // + ConsoleOutputLineTruncateTest.class, // IOConsoleFixedWidthTests.class, // ProcessConsoleManagerTests.class, // ProcessConsoleTests.class, // diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleOutputLineTruncateTest.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleOutputLineTruncateTest.java new file mode 100644 index 00000000000..28737d98061 --- /dev/null +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleOutputLineTruncateTest.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.tests.console; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.eclipse.ui.internal.console.ConsoleOutputLineTruncate; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link ConsoleOutputLineTruncate} handling chunks of input, breaking + * input lines at a specific length limit. + */ +public class ConsoleOutputLineTruncateTest { + + /** + * Parameters of a test for {@link ConsoleOutputLineTruncate}. + * + * @param limit the line length limit for the test + * @param chunks how many times the {@code input} is repeated + * @param repeat how many times {@code output} is repeated in the expected + * output, line breaks are inserted between concatenated + * {@code output} + * @param input input string passed to the tested + * {@link ConsoleOutputLineTruncate} + * @param output expected output, repeated {@code repeat} times + * @param nl the newline character sequences + */ + record Parameters(int limit, int chunks, int repeat, String input, String output, String... nl) { + } + + private static Parameters test(int limit, String input, String output, String... nl) { + return test(limit, 1, 1, input, output, nl); + } + + private static Parameters test(int limit, int chunks, int repeat, String input, String output, String... nl) { + return new Parameters(limit, chunks, repeat, input, output, nl); + } + + private static final Parameters[] TESTS = { + // Unix newlines + test(4, "\n========", "\n==== ...\n", "\n"), + test(10, "========", "========", "\n"), + test(10, 10, 1, "========", "========== ...\n", "\n"), + test(4, "========", "==== ...\n", "\n"), + test(4, 10, 1, "========", "==== ...\n", "\n"), + test(4, "====\n====", "====\n====", "\n"), + test(4, 5, 1, "====\n====", "====\n==== ...\n==== ...\n==== ...\n==== ...\n====", "\n"), + test(2, 5, 1, "=======\n==", "== ...\n== ...\n== ...\n== ...\n== ...\n==", "\n"), + test(2, "====\n====", "== ...\n== ...\n", "\n"), + test(2, 5, 1, "====\n====", "== ...\n== ...\n== ...\n== ...\n== ...\n== ...\n", "\n"), + test(2, "=========", "== ...\n", "\n"), + test(2, "=======\n==", "== ...\n==", "\n"), + test(3, "=========", "=== ...\n", "\n"), + test(3, 5, 1, "=========", "=== ...\n", "\n"), + test(3, "========\n=", "=== ...\n=", "\n"), + test(2, "======\n======", "== ...\n== ...\n", "\n"), + test(2, 5, 1, "======\n======", "== ...\n== ...\n== ...\n== ...\n== ...\n== ...\n", "\n"), + test(3, 3, 1, "========\n=", "=== ...\n=== ...\n=== ...\n=" , "\n"), + test(4, 3, 1, "========\n=", "==== ...\n==== ...\n==== ...\n=", "\n"), + + // Windows newlines + test(4, "\r\n========", "\r\n==== ...\r\n", "\r\n"), + test(10, "========", "========", "\r\n"), + test(10, 10, 1, "========", "========== ...\r\n", "\r\n"), + test(4, "========", "==== ...\r\n", "\r\n"), + test(4, 10, 1, "========", "==== ...\r\n", "\r\n"), + test(4, "====\r\n====", "====\r\n====", "\r\n"), + test(4, 5, 1, "====\r\n====", "====\r\n==== ...\r\n==== ...\r\n==== ...\r\n==== ...\r\n====", "\r\n"), + test(2, 5, 1, "=======\r\n==", "== ...\r\n== ...\r\n== ...\r\n== ...\r\n== ...\r\n==", "\r\n"), + test(2, "====\r\n====", "== ...\r\n== ...\r\n", "\r\n"), + test(2, 5, 1, "====\r\n====", "== ...\r\n== ...\r\n== ...\r\n== ...\r\n== ...\r\n== ...\r\n", "\r\n"), + test(2, "=========", "== ...\r\n", "\r\n"), + test(2, "=======\r\n==", "== ...\r\n==", "\r\n"), + test(3, "=========", "=== ...\r\n", "\r\n"), + test(3, 5, 1, "=========", "=== ...\r\n", "\r\n"), + test(3, "========\r\n=", "=== ...\r\n=", "\r\n"), + test(2, "======\r\n======", "== ...\r\n== ...\r\n", "\r\n"), + test(2, 5, 1, "======\r\n======", "== ...\r\n== ...\r\n== ...\r\n== ...\r\n== ...\r\n== ...\r\n", "\r\n"), + test(3, 3, 1, "========\r\n=", "=== ...\r\n=== ...\r\n=== ...\r\n=", "\r\n"), + test(4, 3, 1, "========\r\n=", "==== ...\r\n==== ...\r\n==== ...\r\n=", "\r\n"), + + // multiple newlines + test(3, 3, 1, "========\r\n=", "=== ...\r\n=== ...\r\n=== ...\r\n=", "\r\n", "\r", "\n"), + test(4, 3, 1, "========\r\n=", "==== ...\r\n==== ...\r\n==== ...\r\n=", "\r\n", "\r", "\n"), + test(3, 3, 1, "========\r\n=", "=== ...\r\n=== ...\r\n=== ...\r\n=", "\r", "\n", "\r\n"), + test(4, 3, 1, "========\r\n=", "==== ...\r\n==== ...\r\n==== ...\r\n=", "\r", "\n", "\r\n"), + }; + + private static Stream tests() { + return Arrays.stream(TESTS).map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("tests") + public void test(Parameters p) { + ConsoleOutputLineTruncate truncate = new ConsoleOutputLineTruncate(p.limit, p.nl); + StringBuilder c = new StringBuilder(); + for (int i = 0; i < p.chunks; ++i) { + StringBuilder s = new StringBuilder(p.input); + CharSequence text = truncate.modify(s); + c.append(text); + } + String expected = repeat(p.output, p.repeat, p.nl[0]); + assertEquals(expected, c.toString()); + } + + private static String repeat(String s, int r, String nl) { + return (nl + s).repeat(r).substring(nl.length()); + } +} diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleOutputLineWrapTest.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleOutputLineWrapTest.java new file mode 100644 index 00000000000..bcd57599237 --- /dev/null +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleOutputLineWrapTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.tests.console; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.eclipse.ui.internal.console.ConsoleOutputLineWrap; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link ConsoleOutputLineWrap} handling chunks of input, breaking input + * lines at a specific length limit. + */ +public class ConsoleOutputLineWrapTest { + + /** + * Parameters of a test for {@link ConsoleOutputLineWrap}. + * + * @param limit the line length limit for the test + * @param chunks how many times the {@code input} is repeated + * @param repeat how many times {@code output} is repeated in the expected + * output, line breaks are inserted between concatenated + * {@code output} + * @param input input string passed to the tested + * {@link ConsoleOutputLineWrap} + * @param output expected output, repeated {@code repeat} times + * @param nl the newline character sequences + */ + record Parameters(int limit, int chunks, int repeat, String input, String output, String... nl) { + } + + private static Parameters test(int limit, String input, String output, String... nl) { + return test(limit, 1, 1, input, output, nl); + } + + private static Parameters test(int limit, int chunks, String input, String output, String... nl) { + return test(limit, chunks, chunks, input, output, nl); + } + + private static Parameters test(int limit, int chunks, int repeat, String input, String output, String... nl) { + return new Parameters(limit, chunks, repeat, input, output, nl); + } + + private static final Parameters[] TESTS = { + // Unix newlines + test(4, "\n========", "\n====\n====", "\n"), + + test(10, "========" , "========" , "\n"), + test(10, 10, 8, "========" , "==========", "\n"), + test( 4, "========" , "====\n====", "\n"), + test( 4, 10, "========" , "====\n====", "\n"), + test( 4, "====\n====" , "====\n====", "\n"), + test( 4, 10, "====\n====" , "====\n====", "\n"), + + test( 2, 10, "=======\n==" , "==\n==\n==\n=\n==", "\n"), + test( 2, "====\n====" , "==\n==\n==\n==" , "\n"), + test( 2, 10, "====\n====" , "==\n==\n==\n==" , "\n"), + test( 2, "=========" , "==\n==\n==\n==\n=", "\n"), + test( 2, "=======\n==" , "==\n==\n==\n=\n==", "\n"), + test( 3, "=========" , "===\n===\n===" , "\n"), + test( 3, 10, "=========" , "===\n===\n===" , "\n"), + test( 3, "========\n=" , "===\n===\n==\n=" , "\n"), + + test( 2, "======\n======", "==\n==\n==\n==\n==\n==", "\n"), + test( 2, 10, "======\n======", "==\n==\n==\n==\n==\n==", "\n"), + + test( 3, 3, 1, "========\n=" , "===\n===\n==\n===\n===\n===\n===\n===\n===\n=", "\n"), + test( 4, 3, 1, "========\n=" , "====\n====\n====\n====\n=\n====\n====\n=\n=" , "\n"), + + // Windows newlines + test(4, "\r\n========", "\r\n====\r\n====", "\r\n"), + + test(10, "========" , "========" , "\r\n"), + test(10, 10, 8, "========" , "==========" , "\r\n"), + test( 4, "========" , "====\r\n====", "\r\n"), + test( 4, 10, "========" , "====\r\n====", "\r\n"), + test( 4, "====\r\n====" , "====\r\n====", "\r\n"), + test( 4, 10, "====\r\n====" , "====\r\n====", "\r\n"), + + test( 2, 10, "=======\r\n==" , "==\r\n==\r\n==\r\n=\r\n==", "\r\n"), + test( 2, "====\r\n====" , "==\r\n==\r\n==\r\n==" , "\r\n"), + test( 2, 10, "====\r\n====" , "==\r\n==\r\n==\r\n==" , "\r\n"), + test( 2, "=========" , "==\r\n==\r\n==\r\n==\r\n=", "\r\n"), + test( 2, "=======\r\n==" , "==\r\n==\r\n==\r\n=\r\n==", "\r\n"), + test( 3, "=========" , "===\r\n===\r\n===" , "\r\n"), + test( 3, 10, "=========" , "===\r\n===\r\n===" , "\r\n"), + test( 3, "========\r\n=" , "===\r\n===\r\n==\r\n=" , "\r\n"), + + test( 2, "======\r\n======", "==\r\n==\r\n==\r\n==\r\n==\r\n==", "\r\n"), + test( 2, 10, "======\r\n======", "==\r\n==\r\n==\r\n==\r\n==\r\n==", "\r\n"), + + test( 3, 3, 1, "========\r\n=" , "===\r\n===\r\n==\r\n===\r\n===\r\n===\r\n===\r\n===\r\n===\r\n=", "\r\n"), + test( 4, 3, 1, "========\r\n=" , "====\r\n====\r\n====\r\n====\r\n=\r\n====\r\n====\r\n=\r\n=" , "\r\n"), + + // multiple newlines + test( 3, 3, 1, "========\r\n=" , "===\r\n===\r\n==\r\n===\r\n===\r\n===\r\n===\r\n===\r\n===\r\n=", "\r\n", "\r", "\n"), + test( 4, 3, 1, "========\r\n=" , "====\r\n====\r\n====\r\n====\r\n=\r\n====\r\n====\r\n=\r\n=" , "\r\n", "\r", "\n"), + + test( 3, 3, 1, "========\r\n=" , "===\r===\r==\r\n===\r===\r===\r\n===\r===\r===\r\n=", "\r", "\n", "\r\n"), + test( 4, 3, 1, "========\r\n=" , "====\r====\r\n====\r====\r=\r\n====\r====\r=\r\n=" , "\r", "\n", "\r\n"), + }; + + private static Stream tests() { + return Arrays.stream(TESTS).map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("tests") + public void test(Parameters p) { + ConsoleOutputLineWrap lineBreak = new ConsoleOutputLineWrap(p.limit, p.nl); + StringBuilder c = new StringBuilder(); + for (int i = 0; i < p.chunks; ++i) { + StringBuilder s = new StringBuilder(p.input); + CharSequence text = lineBreak.modify(s); + c.append(text); + } + String expected = repeat(p.output, p.repeat, p.nl[0]); + assertEquals(expected, c.toString()); + } + + private static String repeat(String s, int r, String nl) { + return (nl + s).repeat(r).substring(nl.length()); + } +} diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java index 945f4c8c8bf..d9393986eeb 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java @@ -80,6 +80,7 @@ */ @ExtendWith(DebugTestExtension.class) public class IOConsoleTests { + /** * The console view used for the running test. Required to obtain access to * consoles {@link StyledText} widget to simulate user input. @@ -761,6 +762,124 @@ public void testTrimSurrogateCharacters() throws Exception { } } + /** + * Check trimming of long lines. + */ + @Test + public void testTrimLongLine() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test trim long line"); + try (IOConsoleOutputStream out = c.getDefaultOutputStream()) { + c.getConsole().setLimitLineLength(false, 8); + c.writeFast("first\n"); + c.writeFast("0123456789\n", out); + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue(c.getDocument().getNumberOfLines() > 2, "Document not filled."); + c.waitForScheduledJobs(); + c.verifyContent("first\n01234567 ...\nlast\n"); + closeConsole(c); + } + } + + /** + * Check that trimming long lines doesn't split at a newline. + */ + @Test + public void testTrimLongLineNewline() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test trim long line with newline"); + try (IOConsoleOutputStream out = c.getDefaultOutputStream()) { + c.getConsole().setLimitLineLength(false, 8); + c.writeFast("first\n"); + c.writeFast("0123456\r\n", out); + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue(c.getDocument().getNumberOfLines() > 2, "Document not filled."); + c.waitForScheduledJobs(); + c.verifyContent("first\n0123456\r\nlast\n"); + closeConsole(c); + } + } + + /** + * Check that trimming long lines doesn't split surrogate pairs, e.g. + * emojis. + */ + @Test + public void testTrimLongLineSurrogateCharacters() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test trim long line with emoji"); + try (IOConsoleOutputStream out = c.getDefaultOutputStream()) { + c.getConsole().setLimitLineLength(false, 8); + c.writeFast("first\n"); + c.writeFast("01234😀😀😀\n", out); + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue(c.getDocument().getNumberOfLines() > 2, "Document not filled."); + c.waitForScheduledJobs(); + c.verifyContent("first\n01234😀 ...\nlast\n"); + closeConsole(c); + } + } + + /** + * Check wrapping of long lines. + */ + @Test + public void testWrapLongLine() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test wrap long line"); + try (IOConsoleOutputStream out = c.getDefaultOutputStream()) { + String nl = c.getConsole().getDocument().getLegalLineDelimiters()[0]; + c.getConsole().setLimitLineLength(true, 8); + c.writeFast("first\n"); + c.writeFast("0123456789\n", out); + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue(c.getDocument().getNumberOfLines() > 2, "Document not filled."); + c.waitForScheduledJobs(); + c.verifyContent("first\n01234567" + nl + "89\nlast\n"); + closeConsole(c); + } + } + + /** + * Check that wrapping long lines doesn't split newlines. + */ + @Test + public void testWrapLongLineNewline() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test wrap long line with newline"); + try (IOConsoleOutputStream out = c.getDefaultOutputStream()) { + c.getConsole().setLimitLineLength(true, 8); + c.writeFast("first\n"); + c.writeFast("0123456\r\n", out); + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue(c.getDocument().getNumberOfLines() > 2, "Document not filled."); + c.waitForScheduledJobs(); + c.verifyContent("first\n0123456\r\nlast\n"); + closeConsole(c); + } + } + + /** + * Check that wrapping long lines doesn't split surrogate pairs, e.g. + * emojis. + */ + @Test + public void testWrapLongLineSurrogateCharacters() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test wrap long line with emoji"); + try (IOConsoleOutputStream out = c.getDefaultOutputStream()) { + String nl = c.getConsole().getDocument().getLegalLineDelimiters()[0]; + c.getConsole().setLimitLineLength(true, 8); + c.writeFast("first\n"); + c.writeFast("0123456😀😀\n", out); + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue(c.getDocument().getNumberOfLines() > 2, "Document not filled."); + c.waitForScheduledJobs(); + c.verifyContent("first\n0123456" + nl + "😀😀\nlast\n"); + closeConsole(c); + } + } + /** * Some extra tests for IOConsolePartitioner. */ diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java index 79ac70b3cd2..3ff13f5e296 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/DebugUIPreferenceInitializer.java @@ -86,6 +86,9 @@ public void initializeDefaultPreferences() { prefs.setDefault(IDebugPreferenceConstants.CONSOLE_LIMIT_CONSOLE_OUTPUT, true); prefs.setDefault(IDebugPreferenceConstants.CONSOLE_LOW_WATER_MARK, 80000); prefs.setDefault(IDebugPreferenceConstants.CONSOLE_HIGH_WATER_MARK, 100000); + prefs.setDefault(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES, false); + prefs.setDefault(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_WRAP, false); + prefs.setDefault(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_LENGTH, 1000); 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); diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java index a0f6b7b4d8b..427f4a8c818 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/ConsolePreferencePage.java @@ -92,6 +92,10 @@ protected void clearErrorMessage() { private BooleanFieldEditor2 fUseBufferSize; private ConsoleIntegerFieldEditor fBufferSizeEditor; + private BooleanFieldEditor2 fLimitLines; + private BooleanFieldEditor2 fLimitLineWrap; + private ConsoleIntegerFieldEditor fLimitLineLength; + private ConsoleIntegerFieldEditor fTabSizeEditor; private BooleanFieldEditor autoScrollLockEditor; @@ -165,6 +169,24 @@ public void widgetSelected(SelectionEvent e) { } ); + fLimitLines = new BooleanFieldEditor2(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES, DebugPreferencesMessages.ConsolePreferencePage_Limit_console_lines, SWT.NONE, getFieldEditorParent()); + addField(fLimitLines); + fLimitLines.getChangeControl(getFieldEditorParent()).addSelectionListener( + new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateLineLimitControls(); + } + } + ); + + fLimitLineWrap = new BooleanFieldEditor2(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_WRAP, DebugPreferencesMessages.ConsolePreferencePage_Limit_console_lines_wrap, SWT.NONE, getFieldEditorParent()); + addField(fLimitLineWrap); + + fLimitLineLength = new ConsoleIntegerFieldEditor(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_LENGTH, DebugPreferencesMessages.ConsolePreferencePage_Limit_console_lines_length, getFieldEditorParent()); + fLimitLineLength.setValidRange(1, Integer.MAX_VALUE - 100000); + addField(fLimitLineLength); + fTabSizeEditor = new ConsoleIntegerFieldEditor(IDebugPreferenceConstants.CONSOLE_TAB_WIDTH, DebugPreferencesMessages.ConsolePreferencePage_12, getFieldEditorParent()); addField(fTabSizeEditor); fTabSizeEditor.setValidRange(1,100); @@ -277,6 +299,7 @@ protected void initialize() { updateWidthEditor(); updateAutoScrollLockEditor(); updateBufferSizeEditor(); + updateLineLimitControls(); updateInterpretCrAsControlCharacterEditor(); updateWordWrapEditorFromConsolePreferences(); } @@ -308,6 +331,17 @@ protected void updateBufferSizeEditor() { fBufferSizeEditor.getLabelControl(getFieldEditorParent()).setEnabled(b.getSelection()); } + /** + * Update enablement for line length limits based on enablement of 'limit + * console lines' editor. + */ + protected void updateLineLimitControls() { + Button b = fLimitLines.getChangeControl(getFieldEditorParent()); + fLimitLineWrap.setEnabled(b.getSelection(), getFieldEditorParent()); + fLimitLineLength.getTextControl(getFieldEditorParent()).setEnabled(b.getSelection()); + fLimitLineLength.getLabelControl(getFieldEditorParent()).setEnabled(b.getSelection()); + } + /** * Update enablement of carriage return interpretation based on general control * character interpretation. @@ -333,6 +367,7 @@ protected void performDefaults() { super.performDefaults(); updateWidthEditor(); updateBufferSizeEditor(); + updateLineLimitControls(); updateInterpretCrAsControlCharacterEditor(); updateElapsedTimePreferences(); @@ -361,6 +396,9 @@ public void propertyChange(PropertyChangeEvent event) { if (fBufferSizeEditor != null && event.getSource() != fBufferSizeEditor) { fBufferSizeEditor.refreshValidState(); } + if (fLimitLineLength != null && event.getSource() != fLimitLineLength) { + fLimitLineLength.refreshValidState(); + } if (fTabSizeEditor != null && event.getSource() != fTabSizeEditor) { fTabSizeEditor.refreshValidState(); } diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java index b9df13a90fe..bb3b0e0b7e9 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.java @@ -31,6 +31,9 @@ public class DebugPreferencesMessages extends NLS { public static String ConsolePreferencePage_Console_width; public static String ConsolePreferencePage_Limit_console_output_1; public static String ConsolePreferencePage_Console_buffer_size__characters___2; + public static String ConsolePreferencePage_Limit_console_lines; + public static String ConsolePreferencePage_Limit_console_lines_wrap; + public static String ConsolePreferencePage_Limit_console_lines_length; public static String ConsolePreferencePage_ConsoleAutoPinEnable; public static String ConsolePreferencePage_The_console_buffer_size_must_be_at_least_1000_characters__1; diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties index 4e8a8e8c256..527067ef407 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/DebugPreferencesMessages.properties @@ -23,6 +23,9 @@ ConsolePreferencePage_Wrap_text_1=Fixed &width console ConsolePreferencePage_Console_width=&Maximum character width: ConsolePreferencePage_Limit_console_output_1=&Limit console output ConsolePreferencePage_Console_buffer_size__characters___2=Console &buffer size (characters): +ConsolePreferencePage_Limit_console_lines=Limit console lines +ConsolePreferencePage_Limit_console_lines_wrap=Wrap console line at limit +ConsolePreferencePage_Limit_console_lines_length=Console line length limit ConsolePreferencePage_ConsoleAutoPinEnable=Pin current and the new Console views when a new Console view is opened ConsolePreferencePage_The_console_buffer_size_must_be_at_least_1000_characters__1=Buffer size must be between 1000 and {0} inclusive. ConsolePreferencePage_console_width=Character width must be between 80 and 1000 inclusive. diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java index 8984d5d6548..66006d38594 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/preferences/IDebugPreferenceConstants.java @@ -73,6 +73,14 @@ public interface IDebugPreferenceConstants { String CONSOLE_LOW_WATER_MARK = "Console.lowWaterMark"; //$NON-NLS-1$ String CONSOLE_HIGH_WATER_MARK = "Console.highWaterMark"; //$NON-NLS-1$ + /** + * Limit for long lines, lines longer than the limit can be truncated or + * wrapped. + */ + String CONSOLE_LIMIT_LINES = "Console.limitLongLines"; //$NON-NLS-1$ + String CONSOLE_LIMIT_LINES_WRAP = "Console.limitLongLinesWrap"; //$NON-NLS-1$ + String CONSOLE_LIMIT_LINES_LENGTH = "Console.limitLongLinesLength"; //$NON-NLS-1$ + /** * Integer preference specifying the number of spaces composing a * tab in the console. diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java index f5b4bee8990..155dcbb4ee1 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/views/console/ProcessConsole.java @@ -534,6 +534,15 @@ public void propertyChange(PropertyChangeEvent evt) { } else { setWaterMarks(-1, -1); } + } else if (property.equals(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES) || property.equals(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_LENGTH) || property.equals(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_WRAP)) { + boolean limitLines = store.getBoolean(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES); + if (limitLines) { + boolean wrap = store.getBoolean(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_WRAP); + int length = store.getInt(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_LENGTH); + setLimitLineLength(wrap, length); + } else { + setLimitLineLength(false, -1); + } } else if (property.equals(IDebugPreferenceConstants.CONSOLE_TAB_WIDTH)) { int tabWidth = store.getInt(IDebugPreferenceConstants.CONSOLE_TAB_WIDTH); setTabWidth(tabWidth); @@ -687,6 +696,12 @@ protected void init() { setWaterMarks(lowWater, highWater); } + if (store.getBoolean(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES)) { + boolean lineWrap = store.getBoolean(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_WRAP); + int lineLimitLength = store.getInt(IDebugPreferenceConstants.CONSOLE_LIMIT_LINES_LENGTH); + setLimitLineLength(lineWrap, lineLimitLength); + } + setHandleControlCharacters(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CONTROL_CHARACTERS)); setCarriageReturnAsControlCharacter(store.getBoolean(IDebugPreferenceConstants.CONSOLE_INTERPRET_CR_AS_CONTROL_CHARACTER)); diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java index 7a03913cea2..91ab534668a 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsole.java @@ -254,8 +254,22 @@ public void setWaterMarks(int low, int high) { } /** - * Check if all streams connected to this console are closed. If so, - * notify the partitioner that this console is finished. + * Sets a line character length limit for the console. If the length limit is + * positive, lines are wrapped or truncated at this length. + * + * @param wrap {@code true} if the line should be wrapped, {@code false} if it + * should be truncated. No effect if {@code length} is negative. + * @param length The length at which lines are wrapped or truncated. Negative if + * the line should be left unchanged. + * @since 3.17 + */ + public void setLimitLineLength(boolean wrap, int length) { + partitioner.setLimitLineLength(wrap, length); + } + + /** + * Check if all streams connected to this console are closed. If so, notify the + * partitioner that this console is finished. */ private void checkFinished() { if (openStreams.isEmpty()) { diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleOutputLineTruncate.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleOutputLineTruncate.java new file mode 100644 index 00000000000..c90aabacbdc --- /dev/null +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleOutputLineTruncate.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.console; + +import org.eclipse.jface.text.MultiStringMatcher; +import org.eclipse.jface.text.MultiStringMatcher.Match; + +/** + * Processes chunks of character sequences. Truncates lines longer than a + * specified length. + */ +public class ConsoleOutputLineTruncate implements IConsoleOutputModifier { + + private static final String DOTS = " ..."; //$NON-NLS-1$ + private final int lineLimit; + private final MultiStringMatcher newlineMatcher; + private final String nl; + private int currentLineLength = 0; + + public ConsoleOutputLineTruncate(int lineLimit, String... newlines) { + this.lineLimit = lineLimit; + newlineMatcher = MultiStringMatcher.create(newlines); + nl = newlines[0]; + } + + @Override + public void reset() { + currentLineLength = 0; + } + + @Override + public CharSequence modify(CharSequence t) { + StringBuilder text = new StringBuilder(t); + Match newlineMatch = newlineMatcher.indexOf(text, 0); + int currentNewline = matchIndex(newlineMatch); + int start = 0; + int end = currentNewline; + while (end >= 0) { + int diff = truncateLine(text, newlineMatch.getText(), start, end); + start = end + newlineMatch.getText().length() + diff; + newlineMatch = newlineMatcher.indexOf(text, start); + end = matchIndex(newlineMatch); + currentLineLength = 0; + } + int n = text.length(); + truncateLine(text, null, start, n); + return text; + } + + private static int matchIndex(Match match) { + return match != null ? match.getOffset() : -1; + } + + private int truncateLine(StringBuilder text, String newlineMatch, int s, int e) { + int previousLineLength = currentLineLength; + int n = e - s; + currentLineLength += n; + int d = 0; + if (previousLineLength > lineLimit) { + int e1 = e; + if (newlineMatch != null) { + e1 += newlineMatch.length(); + n += newlineMatch.length(); + } + text.replace(s, e1, ""); //$NON-NLS-1$ + d = -n; + } else if (currentLineLength > lineLimit) { + int s1 = s + lineLimit - previousLineLength; + if (s1 > 0 && Character.isLowSurrogate(text.charAt(s1))) { + --s1; + } + String dots = DOTS; + if (newlineMatch == null) { + dots += nl; + } + text.replace(s1, e, dots); + n = e - s1; + d = dots.length() - n; + } + return d; + } +} diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleOutputLineWrap.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleOutputLineWrap.java new file mode 100644 index 00000000000..bd39c718537 --- /dev/null +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleOutputLineWrap.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.console; + +import org.eclipse.jface.text.MultiStringMatcher; +import org.eclipse.jface.text.MultiStringMatcher.Match; + +/** + * Processes chunks of character sequences. Wraps lines longer than a specified + * length. + */ +public class ConsoleOutputLineWrap implements IConsoleOutputModifier { + + private final int lineLimit; + private final MultiStringMatcher newlineMatcher; + private final String nl; + private int currentLineLength = 0; + + public ConsoleOutputLineWrap(int lineLimit, String... newlines) { + this.lineLimit = lineLimit; + newlineMatcher = MultiStringMatcher.create(newlines); + nl = newlines[0]; + } + + @Override + public void reset() { + currentLineLength = 0; + } + + @Override + public CharSequence modify(CharSequence t) { + StringBuilder text = new StringBuilder(t); + Match newlineMatch = newlineMatcher.indexOf(text, 0); + int currentNewline = matchIndex(newlineMatch); + int start = 0; + int end = currentNewline; + while (end >= 0) { + int breaks = breakLine(text, start, end); + start = end + newlineMatch.getText().length() + breaks; + newlineMatch = newlineMatcher.indexOf(text, start); + end = matchIndex(newlineMatch); + currentLineLength = 0; + } + int n = text.length(); + breakLine(text, start, n); + return text; + } + + private static int matchIndex(Match match) { + return match != null ? match.getOffset() : -1; + } + + private int breakLine(StringBuilder text, int s, int e) { + int previousLineLength = currentLineLength; + currentLineLength += e - s; + int b = 0; + if (currentLineLength > lineLimit) { + int s1 = s + lineLimit - previousLineLength; + char c = text.charAt(s1); + if (s1 > 0 && (Character.isLowSurrogate(c))) { + --s1; + } + int e1 = e; + for (int i = s1; i < e1; i += lineLimit) { + text.insert(i, nl); + currentLineLength = e1 - i; + i += nl.length(); + e1 += nl.length(); + b += nl.length(); + } + } + return b; + } +} diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IConsoleOutputModifier.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IConsoleOutputModifier.java new file mode 100644 index 00000000000..48a5b4ba8b7 --- /dev/null +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IConsoleOutputModifier.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2026 Simeon Andreev and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Simeon Andreev - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.console; + +/** + * Processes character sequences split into chunks. Modifies long lines in the + * input. Lines can span multiple chunks, chunks can contain multiple lines. + */ +public interface IConsoleOutputModifier { + + /** + * Resets state to handle a new set of chunks. + */ + void reset(); + + /** + * Processes a chunk, modifying long lines. + * + * @param chunk the current chunk of input + * @return the potentially modified chunk + */ + CharSequence modify(CharSequence chunk); +} diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java index dd12bf2c546..bc9f3c46da4 100644 --- a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java @@ -162,6 +162,21 @@ private enum DocUpdateType { * this many characters are remain in console. */ private int lowWaterMark = -1; + /** + * If {@link #lineLengthLimit} is non-negative, will either wrap or cut lines + * depending on this value. + */ + private volatile boolean lineLimitWrap = false; + /** + * Line limit length, lines longer than this are either wrapped or truncated. + * Negative to disable. + */ + private volatile int lineLengthLimit = -1; + /** + * Truncates or wraps console lines if {@link #lineLengthLimit} is positive, + * depending on {@link #lineLimitWrap}. + */ + private IConsoleOutputModifier lineModifier; /** The partitioned {@link IOConsole}. */ private final IOConsole console; @@ -219,6 +234,7 @@ public void connect(IDocument doc) { inputPartitions = new ArrayList<>(); document = doc; legalLineDelimiterMatcher = MultiStringMatcher.create(document.getLegalLineDelimiters()); + setLineModifier(); } } } @@ -269,6 +285,20 @@ public void setWaterMarks(int low, int high) { ConsolePlugin.getStandardDisplay().asyncExec(this::checkBufferSize); } + /** + * Set handling for long lines. Lines can be wrapped, truncated or left + * unchanged. + * + * @param wrap whether to wrap the line or truncate line limit + * @param length length at which to wrap or truncate lines, negative to disable + * @see IOConsole#setLimitLineLength(boolean, int) + */ + public void setLimitLineLength(boolean wrap, int length) { + lineLimitWrap = wrap; + lineLengthLimit = length; + setLineModifier(); + } + /** * Notification from the console that all of its streams have been closed. */ @@ -793,6 +823,17 @@ private void processPendingPartitions() { if (pendingCopy.isEmpty()) { return; } + + if (lineModifier != null) { + List modified = new ArrayList<>(pendingCopy.size()); + for (PendingPartition partition : pendingCopy) { + CharSequence t = lineModifier.modify(partition.text); + PendingPartition m = new PendingPartition(partition.stream, t); + modified.add(m); + } + pendingCopy = modified; + } + int pendingSize = 0; IOConsoleOutputStream stream = pendingCopy.get(0).stream; for (PendingPartition p : pendingCopy) { @@ -1508,4 +1549,14 @@ private void checkPartitions() { Assert.isTrue(knownInputPartitions.isEmpty()); } } + + private void setLineModifier() { + if (document != null && lineLengthLimit > 0) { + String[] lineDelimiters = document.getLegalLineDelimiters(); + lineModifier = lineLimitWrap ? new ConsoleOutputLineWrap(lineLengthLimit, lineDelimiters) + : new ConsoleOutputLineTruncate(lineLengthLimit, lineDelimiters); + } else { + lineModifier = null; + } + } }