From 5bb38a1b0ec0fa70ceb3dd4b811c40e1be0aebe4 Mon Sep 17 00:00:00 2001 From: insjang Date: Wed, 2 Sep 2026 21:35:52 +0900 Subject: [PATCH 1/2] Terminal: erase in the current background, and forget a pending wrap on any cursor move Two places where the emulator departed from what programs written for xterm expect. Back color erase. Erasing wrote cells with no style, so they took the terminal's own background. Every colour terminal (xterm's "bce") erases in the background currently set, and programs rely on it: a run of a line is coloured by setting a background and erasing to the end of it, which is how a status line, a selection highlight or a table cell gets painted wider than its text. Without it such a run kept the terminal colour from its last character on. EL, ED and ECH now erase in the current background, and so do the cells that ICH opens and DCH uncovers, as in xterm; only the background carries over, not bold or underline. Pending wrap. With deferred wrapping (DECAWM as xterm does it) a character written to the last column leaves the cursor there with a wrap pending. DEC STD 070 lists what clears that state: CR, LF, IND, RI, NEL, the cursor movement sequences, and the erase, insert and delete sequences. The emulator cleared it only on a column move, so a line feed or a cursor-down after a full row still wrapped the next character a row further down and marked an unrelated row as wrapped - which showed up as unrelated rows joined when copying from a full screen program. Tests cover the backgrounds of each erase and of ICH/DCH, and the reset of the pending wrap by LF, cursor line moves and ECH. --- .../META-INF/MANIFEST.MF | 2 +- .../emulator/VT100EmulatorBackend.java | 60 ++++++++------ .../emulator/VT100EmulatorBackendTest.java | 78 +++++++++++++++++++ 3 files changed, 115 insertions(+), 25 deletions(-) diff --git a/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF b/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF index ff5a3fb72be..0c618bbd154 100644 --- a/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF +++ b/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.terminal.control; singleton:=true -Bundle-Version: 1.1.200.qualifier +Bundle-Version: 1.1.300.qualifier Bundle-Activator: org.eclipse.terminal.internal.control.impl.TerminalPlugin Bundle-Vendor: %providerName Bundle-Localization: plugin diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java index 4dfc1f1b560..62379ad64b8 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackend.java @@ -165,10 +165,7 @@ public void insertCharacters(int charactersToInsert) { TerminalStyle style = fTerminal.getStyle(line, col - n); fTerminal.setChar(line, col, c, style); } - int last = Math.min(fCursorColumn + n, fColumns); - for (int col = fCursorColumn; col < last; col++) { - fTerminal.setChar(line, col, '\000', null); - } + eraseCells(line, fCursorColumn, Math.min(fCursorColumn + n, fColumns)); } } @@ -177,7 +174,7 @@ public void eraseToEndOfScreen() { synchronized (fTerminal) { eraseLineToEnd(); for (int line = toAbsoluteLine(fCursorLine + 1); line < toAbsoluteLine(fLines); line++) { - fTerminal.cleanLine(line); + eraseLine(line); } } @@ -187,7 +184,7 @@ public void eraseToEndOfScreen() { public void eraseToCursor() { synchronized (fTerminal) { for (int line = toAbsoluteLine(0); line < toAbsoluteLine(fCursorLine); line++) { - fTerminal.cleanLine(line); + eraseLine(line); } eraseLineToCursor(); } @@ -197,7 +194,7 @@ public void eraseToCursor() { public void eraseAll() { synchronized (fTerminal) { for (int line = toAbsoluteLine(0); line < toAbsoluteLine(fLines); line++) { - fTerminal.cleanLine(line); + eraseLine(line); } } } @@ -205,27 +202,45 @@ public void eraseAll() { @Override public void eraseLine() { synchronized (fTerminal) { - fTerminal.cleanLine(toAbsoluteLine(fCursorLine)); + eraseLine(toAbsoluteLine(fCursorLine)); } } + private void eraseLine(int line) { + fTerminal.cleanLine(line); + eraseCells(line, 0, fColumns); + } + @Override public void eraseLineToEnd() { synchronized (fTerminal) { - int line = toAbsoluteLine(fCursorLine); - for (int col = fCursorColumn; col < fColumns; col++) { - fTerminal.setChar(line, col, '\000', null); - } + eraseCells(toAbsoluteLine(fCursorLine), fCursorColumn, fColumns); + } + } + + /** + * Erasing leaves behind the background a program is writing in, not the one the + * terminal started with (xterm's "back color erase", which every terminal that + * supports colour implements). Colouring a run of a line by setting a background + * and erasing to the end of it is how a program paints anything wider than the + * characters it has to put there, and without this such a line keeps the + * terminal's own colour from its last character on. xterm applies this to every + * erase (EL, ED, ECH) and to the cells insertion and deletion uncover (ICH, DCH). + */ + private void eraseCells(int line, int from, int to) { + fWrapPending = false; // erasing, inserting and deleting all forget a pending wrap + // Only the background carries over; what is erased is not bold or underlined. + TerminalStyle plain = getDefaultStyle(); + TerminalStyle erased = plain == null || fStyle == null ? fStyle : plain.setBackground(fStyle); + for (int col = from; col < to; col++) { + fTerminal.setChar(line, col, '\000', erased); } } @Override public void eraseLineToCursor() { synchronized (fTerminal) { - int line = toAbsoluteLine(fCursorLine); - for (int col = 0; col <= fCursorColumn; col++) { - fTerminal.setChar(line, col, '\000', null); - } + eraseCells(toAbsoluteLine(fCursorLine), 0, fCursorColumn + 1); } } @@ -251,10 +266,7 @@ public void deleteCharacters(int n) { TerminalStyle style = fTerminal.getStyle(line, col); fTerminal.setChar(line, col - n, c, style); } - int first = Math.max(fCursorColumn, fColumns - n); - for (int col = first; col < fColumns; col++) { - fTerminal.setChar(line, col, '\000', null); - } + eraseCells(line, Math.max(fCursorColumn, fColumns - n), fColumns); } } @@ -353,6 +365,7 @@ private int doLineWrap() { * MUST be called from a synchronized block! */ private void doNewline() { + fWrapPending = false; // DEC STD 070: any move of the cursor forgets a pending wrap if (fCursorLine == fScrollRegion.getBottomLine()) { scrollUp(1); } else if (fCursorLine + 1 >= fLines) { @@ -374,6 +387,7 @@ public void processNewline() { } private void doReverseLineFeed() { + fWrapPending = false; if (fCursorLine == fScrollRegion.getTopLine()) { scrollDown(1); } else { @@ -436,6 +450,7 @@ public void setCursorLine(int targetLine) { targetLine = fLines - 1; } fCursorLine = targetLine; + fWrapPending = false; // We make the assumption that nobody is changing the // terminal cursor except this class! // This assumption gives a huge performance improvement @@ -505,10 +520,7 @@ public void scrollDown(int n) { public void eraseCharacters(int n) { synchronized (fTerminal) { int line = toAbsoluteLine(fCursorLine); - int end = Math.min(fCursorColumn + n, fColumns); - for (int col = fCursorColumn; col < end; col++) { - fTerminal.setChar(line, col, '\000', null); - } + eraseCells(line, fCursorColumn, Math.min(fCursorColumn + n, fColumns)); } } } diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java index aecbc640616..42d5e445c35 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/emulator/VT100EmulatorBackendTest.java @@ -17,6 +17,7 @@ package org.eclipse.terminal.internal.emulator; import static org.eclipse.terminal.model.TerminalColor.BLACK; +import static org.eclipse.terminal.model.TerminalColor.GREEN; import static org.eclipse.terminal.model.TerminalColor.WHITE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -1086,4 +1087,81 @@ public void testNewlineInTopAnchoredScrollRegionCurrentlyDiscardsTopLine() { assertNull(term.getChars(3)); assertEquals("4444", new String(term.getChars(4))); // footer below the region is untouched } + + @Test + public void testEraseKeepsBackground() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(4); + vt100.setDimensions(4, 8); + vt100.setDefaultStyle(TerminalStyle.getStyle(WHITE, BLACK)); + TerminalStyle onGreen = TerminalStyle.getStyle(WHITE, GREEN).setBold(true); + // a program paints a run wider than its text by setting a background and erasing + vt100.setStyle(onGreen); + vt100.setCursor(0, 0); + vt100.appendString("ab"); + vt100.eraseLineToEnd(); + assertEquals(GREEN, term.getStyle(0, 5).getBackgroundTerminalColor()); + assertEquals(GREEN, term.getStyle(0, 7).getBackgroundTerminalColor()); + assertFalse(term.getStyle(0, 5).isBold()); // only the background carries over + // the same for erase to cursor, whole line, ECH, and the cells ICH and DCH uncover + vt100.setCursor(1, 3); + vt100.eraseLineToCursor(); + assertEquals(GREEN, term.getStyle(1, 0).getBackgroundTerminalColor()); + vt100.setCursor(2, 0); + vt100.eraseLine(); + assertEquals(GREEN, term.getStyle(2, 7).getBackgroundTerminalColor()); + vt100.setStyle(TerminalStyle.getStyle(WHITE, BLACK)); + vt100.setCursor(3, 0); + vt100.appendString("0123456"); // one short of the margin, so as not to wrap + vt100.setStyle(onGreen); + vt100.setCursor(3, 1); + vt100.eraseCharacters(2); + assertEquals(GREEN, term.getStyle(3, 2).getBackgroundTerminalColor()); + assertEquals(BLACK, term.getStyle(3, 3).getBackgroundTerminalColor()); + vt100.setCursor(3, 4); + vt100.insertCharacters(1); + assertEquals(GREEN, term.getStyle(3, 4).getBackgroundTerminalColor()); + assertEquals('4', term.getChar(3, 5)); + vt100.deleteCharacters(1); + assertEquals(GREEN, term.getStyle(3, 7).getBackgroundTerminalColor()); + // erasing in the default background stays default + vt100.setStyle(TerminalStyle.getStyle(WHITE, BLACK)); + vt100.setCursor(0, 0); + vt100.eraseToEndOfScreen(); + assertEquals(BLACK, term.getStyle(2, 7).getBackgroundTerminalColor()); + } + + @Test + public void testPendingWrapForgottenByCursorMove() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(3); + vt100.setDimensions(3, 5); + vt100.setVT100LineWrapping(true); // xterm's deferred wrap, as the process connector sets it + vt100.setCursor(0, 0); + vt100.appendString("abcde"); // fills the row: the wrap is pending, not done + // DEC STD 070: a line feed, a cursor move or an erase forgets the pending wrap + vt100.processNewline(); + vt100.appendString("x"); + assertEquals('x', term.getChar(1, 4)); + assertFalse(term.isWrappedLine(0)); + assertFalse(term.isWrappedLine(1)); + vt100.setCursor(0, 4); + vt100.appendString("e"); + vt100.setCursorLine(2); + vt100.appendString("y"); + assertEquals('y', term.getChar(2, 4)); + vt100.setCursor(0, 4); + vt100.appendString("e"); + vt100.eraseCharacters(1); + vt100.appendString("z"); + assertEquals('z', term.getChar(0, 4)); + assertEquals(0, vt100.getCursorLine()); + // only writing on is a wrap + vt100.setCursor(0, 4); + vt100.appendString("ef"); + assertTrue(term.isWrappedLine(0)); + assertEquals('f', term.getChar(1, 0)); + } } From b2a6a48dc46f916d2d0c23dc6ba5f69105fcfb6a Mon Sep 17 00:00:00 2001 From: insjang Date: Thu, 3 Sep 2026 12:37:15 +0900 Subject: [PATCH 2/2] Terminal: retrigger CI (previous run failed on unrelated infocenter-app: Java heap space)