From 94faeaf8e1e521e3edef1434aa3917b42f5d8bd7 Mon Sep 17 00:00:00 2001 From: insjang Date: Wed, 2 Sep 2026 21:25:24 +0900 Subject: [PATCH 1/3] Terminal: count East Asian Wide characters as two columns The emulator assumed every character occupies one cell, so Hangul, Han and Kana text, fullwidth forms and emoji were placed one column short per character and the screen fell apart as soon as a program laid text out for a real terminal (line editors, curses UIs, Ink based CLIs). Add CharWidth, a UAX #11 East Asian Width lookup: Wide and Fullwidth count as two columns, combining marks and controls as zero, Ambiguous as one, as UAX #11 recommends outside an East Asian legacy context. The emulator advances the cursor by that width and stores a NUL filler in the second cell of a wide character, never splits one across the right margin, blanks the other half when either half is overwritten and counts insert mode in cells. The renderer skips the fillers so a fixed width font draws a wide glyph over both cells, falls back to placing each character at its own cell when the font does not advance exactly one cell per column, and draws a character beyond the BMP whole. A partial repaint that starts on the second cell of a wide character is widened to its first, and copying drops the fillers. Tests cover the width table, placement, the margin, overwriting halves and insert mode. --- .../META-INF/MANIFEST.MF | 2 +- .../emulator/VT100EmulatorBackend.java | 90 ++++++++++++- .../terminal/internal/model/CharWidth.java | 122 ++++++++++++++++++ .../textcanvas/AbstractTextCanvasModel.java | 14 +- .../internal/textcanvas/TextCanvas.java | 25 ++++ .../internal/textcanvas/TextLineRenderer.java | 73 ++++++++++- .../emulator/VT100EmulatorBackendTest.java | 89 +++++++++++++ .../terminal/internal/model/AllTestSuite.java | 1 + .../internal/model/CharWidthTest.java | 69 ++++++++++ 9 files changed, 473 insertions(+), 12 deletions(-) create mode 100644 terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/CharWidth.java create mode 100644 terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/CharWidthTest.java 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..e4e13e270b5 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 @@ -14,6 +14,7 @@ *******************************************************************************/ package org.eclipse.terminal.internal.emulator; +import org.eclipse.terminal.internal.model.CharWidth; import org.eclipse.terminal.model.ITerminalTextData; import org.eclipse.terminal.model.TerminalStyle; @@ -311,7 +312,7 @@ public void appendString(String buffer) { synchronized (fTerminal) { char[] chars = buffer.toCharArray(); if (fInsertMode) { - insertCharacters(chars.length); + insertCharacters(CharWidth.ofString(buffer)); // room in cells, not characters } int line = toAbsoluteLine(fCursorLine); int i = 0; @@ -319,10 +320,50 @@ public void appendString(String buffer) { if (fWrapPending) { line = doLineWrap(); } - int n = Math.min(fColumns - fCursorColumn, chars.length - i); - fTerminal.setChars(line, fCursorColumn, chars, i, n, fStyle); - int col = fCursorColumn + n; - i += n; + int room = fColumns - fCursorColumn; + int col; + int n = narrowRun(chars, i, room); + if (n > 0) { + breakWideChar(line, fCursorColumn); + breakWideChar(line, fCursorColumn + n - 1); + fTerminal.setChars(line, fCursorColumn, chars, i, n, fStyle); + col = fCursorColumn + n; + i += n; + } else { + int codePoint = Character.codePointAt(chars, i); + int charsUsed = Character.charCount(codePoint); + int width = CharWidth.of(codePoint); + if (width == 0) { + // combining marks and other non-printing code points occupy no cell + i += charsUsed; + continue; + } + // a surrogate pair cannot share a cell, so it always takes two + if (charsUsed == 2) { + width = 2; + } + if (width > room) { + if (fCursorColumn > 0) { + // a wide character is never split across the right margin + line = doLineWrap(); + continue; + } + // terminal narrower than the character itself + width = room; + } + breakWideChar(line, fCursorColumn); + breakWideChar(line, fCursorColumn + width - 1); + if (charsUsed == 2) { + fTerminal.setChars(line, fCursorColumn, chars, i, 2, fStyle); + } else { + fTerminal.setChar(line, fCursorColumn, chars[i], fStyle); + if (width == 2) { + fTerminal.setChar(line, fCursorColumn + 1, '\000', fStyle); + } + } + col = fCursorColumn + width; + i += charsUsed; + } // wrap needed? if (col == fColumns) { if (fVT100LineWrapping) { @@ -339,6 +380,45 @@ public void appendString(String buffer) { } } + /** + * A wide character owns two cells. Overwriting either one leaves the other + * stranded: a filler with nothing in front of it, or a glyph that now spills + * over whatever was written next to it. Blanking the partner before the write + * goes in keeps the line honest, which is what a terminal is expected to do. + */ + private void breakWideChar(int line, int col) { + if (col < 0 || col >= fColumns) { + return; + } + char c = fTerminal.getChar(line, col); + if (c == '\000') { + if (col > 0 && CharWidth.of(fTerminal.getChar(line, col - 1)) == 2) { + blank(line, col - 1); + } + } else if (CharWidth.of(c) == 2 && col + 1 < fColumns && fTerminal.getChar(line, col + 1) == '\000') { + blank(line, col + 1); + } + } + + private void blank(int line, int col) { + fTerminal.setChar(line, col, ' ', fTerminal.getStyle(line, col)); + } + + /** + * Length of the run of characters starting at offset that each + * occupy exactly one cell, so that they can be copied in one block. Capped at + * max cells. Zero when the run does not start with such a + * character, which sends the caller down the code point by code point path. + */ + private static int narrowRun(char[] chars, int offset, int max) { + int n = 0; + while (n < max && offset + n < chars.length && !Character.isSurrogate(chars[offset + n]) + && CharWidth.of(chars[offset + n]) == 1) { + n++; + } + return n; + } + private int doLineWrap() { int line; line = toAbsoluteLine(fCursorLine); diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/CharWidth.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/CharWidth.java new file mode 100644 index 00000000000..6966ba67a11 --- /dev/null +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/CharWidth.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * All rights reserved. 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 + *******************************************************************************/ +package org.eclipse.terminal.internal.model; + +/** + * Display width of a code point, following Unicode Standard Annex #11 + * (East Asian Width). Used by the emulator to keep its column arithmetic in + * step with what a terminal application assumes. + *

+ * East Asian Wide (W) and Fullwidth (F) count as two columns. Ambiguous (A) is + * treated as narrow, as UAX #11 recommends for a context with no East Asian + * legacy encoding. Combining marks and non-printing characters count as zero. + */ +public final class CharWidth { + + private CharWidth() { + } + + /** + * Wide and Fullwidth ranges from EastAsianWidth-17.0.0.txt, as inclusive + * [start, end] pairs in ascending order. Everything not listed here defaults + * to Narrow, which is what the file's {@code @missing} line specifies. + */ + private static final int[] WIDE_RANGES = { + 0x01100, 0x0115F, 0x0231A, 0x0231B, 0x02329, 0x0232A, 0x023E9, 0x023EC, + 0x023F0, 0x023F0, 0x023F3, 0x023F3, 0x025FD, 0x025FE, 0x02614, 0x02615, + 0x02630, 0x02637, 0x02648, 0x02653, 0x0267F, 0x0267F, 0x0268A, 0x0268F, + 0x02693, 0x02693, 0x026A1, 0x026A1, 0x026AA, 0x026AB, 0x026BD, 0x026BE, + 0x026C4, 0x026C5, 0x026CE, 0x026CE, 0x026D4, 0x026D4, 0x026EA, 0x026EA, + 0x026F2, 0x026F3, 0x026F5, 0x026F5, 0x026FA, 0x026FA, 0x026FD, 0x026FD, + 0x02705, 0x02705, 0x0270A, 0x0270B, 0x02728, 0x02728, 0x0274C, 0x0274C, + 0x0274E, 0x0274E, 0x02753, 0x02755, 0x02757, 0x02757, 0x02795, 0x02797, + 0x027B0, 0x027B0, 0x027BF, 0x027BF, 0x02B1B, 0x02B1C, 0x02B50, 0x02B50, + 0x02B55, 0x02B55, 0x02E80, 0x02E99, 0x02E9B, 0x02EF3, 0x02F00, 0x02FD5, + 0x02FF0, 0x0303E, 0x03041, 0x03096, 0x03099, 0x030FF, 0x03105, 0x0312F, + 0x03131, 0x0318E, 0x03190, 0x031E5, 0x031EF, 0x0321E, 0x03220, 0x03247, + 0x03250, 0x0A48C, 0x0A490, 0x0A4C6, 0x0A960, 0x0A97C, 0x0AC00, 0x0D7A3, + 0x0F900, 0x0FAFF, 0x0FE10, 0x0FE19, 0x0FE30, 0x0FE52, 0x0FE54, 0x0FE66, + 0x0FE68, 0x0FE6B, 0x0FF01, 0x0FF60, 0x0FFE0, 0x0FFE6, 0x16FE0, 0x16FE4, + 0x16FF0, 0x16FF6, 0x17000, 0x18CD5, 0x18CFF, 0x18D1E, 0x18D80, 0x18DF2, + 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB, 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, + 0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155, 0x1B164, 0x1B167, + 0x1B170, 0x1B2FB, 0x1D300, 0x1D356, 0x1D360, 0x1D376, 0x1F004, 0x1F004, + 0x1F0CF, 0x1F0CF, 0x1F18E, 0x1F18E, 0x1F191, 0x1F19A, 0x1F200, 0x1F202, + 0x1F210, 0x1F23B, 0x1F240, 0x1F248, 0x1F250, 0x1F251, 0x1F260, 0x1F265, + 0x1F300, 0x1F320, 0x1F32D, 0x1F335, 0x1F337, 0x1F37C, 0x1F37E, 0x1F393, + 0x1F3A0, 0x1F3CA, 0x1F3CF, 0x1F3D3, 0x1F3E0, 0x1F3F0, 0x1F3F4, 0x1F3F4, + 0x1F3F8, 0x1F43E, 0x1F440, 0x1F440, 0x1F442, 0x1F4FC, 0x1F4FF, 0x1F53D, + 0x1F54B, 0x1F54E, 0x1F550, 0x1F567, 0x1F57A, 0x1F57A, 0x1F595, 0x1F596, + 0x1F5A4, 0x1F5A4, 0x1F5FB, 0x1F64F, 0x1F680, 0x1F6C5, 0x1F6CC, 0x1F6CC, + 0x1F6D0, 0x1F6D2, 0x1F6D5, 0x1F6D8, 0x1F6DC, 0x1F6DF, 0x1F6EB, 0x1F6EC, + 0x1F6F4, 0x1F6FC, 0x1F7E0, 0x1F7EB, 0x1F7F0, 0x1F7F0, 0x1F90C, 0x1F93A, + 0x1F93C, 0x1F945, 0x1F947, 0x1F9FF, 0x1FA70, 0x1FA7C, 0x1FA80, 0x1FA8A, + 0x1FA8E, 0x1FAC6, 0x1FAC8, 0x1FAC8, 0x1FACD, 0x1FADC, 0x1FADF, 0x1FAEA, + 0x1FAEF, 0x1FAF8, 0x20000, 0x2FFFD, 0x30000, 0x3FFFD + }; + + /** @return 0 for combining and non-printing, 2 for East Asian W/F, else 1 */ + public static int of(int codePoint) { + if (codePoint < 0x0080) { + return codePoint < 0x20 || codePoint == 0x7F ? 0 : 1; + } + return isZeroWidth(codePoint) ? 0 : isWide(codePoint) ? 2 : 1; + } + + /** @return the total display width of {@code s} */ + public static int ofString(String s) { + return s.codePoints().map(CharWidth::of).sum(); + } + + /** + * A {@code '\000'} means one of two things in a line of cells: the filler that + * a wide character puts in the cell it also covers, which carries no text of + * its own, or a cell that was never written or has been erased, which reads as + * a space. + * + * @return whether the cell at {@code index} is the filler of the character + * before it + */ + public static boolean isFiller(CharSequence text, int index) { + return text.charAt(index) == '\000' && index > 0 && of(Character.codePointBefore(text, index)) == 2; + } + + private static boolean isZeroWidth(int codePoint) { + // Hangul conjoining jamo vowels and trailing consonants: EAW lists them as + // neutral, but they combine into the leading consonant before them. + if (codePoint >= 0x1160 && codePoint <= 0x11FF) { + return true; + } + switch (Character.getType(codePoint)) { + case Character.NON_SPACING_MARK: + case Character.ENCLOSING_MARK: + case Character.CONTROL: + case Character.FORMAT: + return true; + default: + return false; + } + } + + private static boolean isWide(int codePoint) { + int lo = 0, hi = WIDE_RANGES.length / 2 - 1; + while (lo <= hi) { + int mid = (lo + hi) >>> 1, i = mid * 2; + if (codePoint < WIDE_RANGES[i]) { + hi = mid - 1; + } else if (codePoint > WIDE_RANGES[i + 1]) + lo = mid + 1; + else { + return true; + } + } + return false; + } +} diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java index db57118ddc5..09acdc6d71a 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java @@ -21,6 +21,7 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.swt.graphics.Point; import org.eclipse.terminal.connector.Logger; +import org.eclipse.terminal.internal.model.CharWidth; import org.eclipse.terminal.model.ITerminalTextDataReadOnly; import org.eclipse.terminal.model.ITerminalTextDataSnapshot; import org.eclipse.terminal.model.TextRange; @@ -427,8 +428,17 @@ private static String scrubLine(String text) { } text = text.substring(0, i + 1); // - // null means space - return text.replace('\000', ' '); + // null means space, unless it is the filler of a wide character + StringBuilder scrubbed = new StringBuilder(text.length()); + for (int j = 0; j < text.length(); j++) { + char c = text.charAt(j); + if (c != '\000') { + scrubbed.append(c); + } else if (!CharWidth.isFiller(text, j)) { + scrubbed.append(' '); + } + } + return scrubbed.toString(); } /** diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java index 92f924be1a7..9d941c9099d 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java @@ -43,6 +43,7 @@ import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.terminal.control.ITerminalMouseListener; +import org.eclipse.terminal.internal.model.CharWidth; import org.eclipse.terminal.model.ITerminalTextDataReadOnly; import org.eclipse.terminal.model.TerminalColor; @@ -535,9 +536,33 @@ protected void repaintRange(int col, int line, int width, int height) { @Override protected void drawLine(GC gc, int line, int x, int y, int colFirst, int colLast) { + // A wide character spans two cells and has to be drawn from the first of + // them. When the damaged area starts on the second one, widen the range so + // the glyph is drawn from its own origin. Clipping then keeps only the half + // that was actually damaged, which is the half that needed repainting. + if (isFillerCell(line, colFirst)) { + colFirst--; + x -= getCellWidth(); + } + // Same at the other end: a wide character starting in the last cell of the + // range would be cut in half by the edge of the range. + if (isFillerCell(line, colLast)) { + colLast++; + } fCellRenderer.drawLine(fCellCanvasModel, gc, line, x, y, colFirst, colLast); } + /** + * @return whether the cell holds the second half of a wide character + */ + private boolean isFillerCell(int line, int col) { + ITerminalTextDataReadOnly text = fCellCanvasModel.getTerminalText(); + if (col <= 0 || col >= text.getWidth() || line < 0 || line >= text.getHeight()) { + return false; + } + return text.getChar(line, col) == '\000' && CharWidth.of(text.getChar(line, col - 1)) == 2; + } + @Override protected Color getTerminalBackgroundColor(Device device) { return fCellRenderer.getDefaultBackgroundColor(); diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java index 8df74b82b22..a225b8486d2 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java @@ -26,6 +26,7 @@ import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.terminal.connector.Logger; +import org.eclipse.terminal.internal.model.CharWidth; import org.eclipse.terminal.model.ITerminalTextDataReadOnly; import org.eclipse.terminal.model.LineSegment; import org.eclipse.terminal.model.TerminalColor; @@ -159,19 +160,83 @@ private void drawText(GC gc, int x, int y, int colFirst, int col, String text) { // draw the background // TODO why does this not work??????? // gc.fillRectangle(x,y,fStyleMap.getFontWidth()*text.length(),fStyleMap.getFontHeight()); + int xx = x + offset; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); - int xx = x + offset + i * fStyleMap.getFontWidth(); + int cells = cellsAt(text, i); // TODO why do I have to draw the background character by character?????? - gc.fillRectangle(xx, y, fStyleMap.getFontWidth(), fStyleMap.getFontHeight()); + gc.fillRectangle(xx, y, cells * fStyleMap.getFontWidth(), fStyleMap.getFontHeight()); if (c != ' ' && c != '\000') { gc.drawString(String.valueOf(c), fStyleMap.getCharOffset(c) + xx, y, false); } + xx += cells * fStyleMap.getFontWidth(); } } else { - text = text.replace('\000', ' '); - gc.drawString(text, x + offset, y, false); + // One call keeps whatever the font does with the run, ligatures included, + // but only while it advances exactly one cell per column. A character the + // font does not have is drawn from somewhere else and rarely does, and + // then everything after it on the line sits in the wrong column. + String drawn = withoutFillers(text); + if (gc.textExtent(drawn).x == text.length() * getCellWidth()) { + gc.drawString(drawn, x + offset, y, false); + } else { + drawCellByCell(gc, x + offset, y, text); + } + } + } + + /** + * Puts every character at the start of its own cell, so the columns hold no + * matter what the font makes of it. A wide character is left to cover the cell + * of the filler that follows it. + */ + private void drawCellByCell(GC gc, int x, int y, String text) { + // The whole run at once, because the characters are drawn over it one at a + // time and the cells between them would otherwise keep what was there before. + gc.fillRectangle(x, y, text.length() * getCellWidth(), getCellHeight()); + for (int i = 0; i < text.length();) { + // A character beyond the BMP is two chars in two cells, and has to be + // drawn whole: half of a surrogate pair is no character at all. + int n = Character.charCount(text.codePointAt(i)); + char c = text.charAt(i); + if (c != ' ' && c != '\000') { + gc.drawString(text.substring(i, i + n), x + i * getCellWidth(), y, true); + } + i += n; + } + } + + /** + * Cells taken up by the character at index: none for the filler of + * a wide character, since the character it belongs to already covers it, two + * for a wide character, one for anything else. + */ + private static int cellsAt(String text, int index) { + if (CharWidth.isFiller(text, index)) { + return 0; + } + int codePoint = text.codePointAt(index); + // a surrogate pair takes two cells whatever its width, as it is stored + return Character.charCount(codePoint) == 2 || CharWidth.of(codePoint) == 2 ? 2 : 1; + } + + /** + * The text as it should be handed to a fixed width font: fillers dropped, since + * the wide character before them already spans their cell, and every other null + * turned into the space it stands for. What is left lines up column for column, + * as long as the font draws a wide character in exactly two cells. + */ + private static String withoutFillers(String text) { + StringBuilder drawn = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c != '\000') { + drawn.append(c); + } else if (!CharWidth.isFiller(text, i)) { + drawn.append(' '); + } } + return drawn.toString(); } /** 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..c53ea81f425 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 @@ -1086,4 +1086,93 @@ public void testNewlineInTopAnchoredScrollRegionCurrentlyDiscardsTopLine() { assertNull(term.getChars(3)); assertEquals("4444", new String(term.getChars(4))); // footer below the region is untouched } + + @Test + public void testAppendStringWide() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(4); + vt100.setDimensions(4, 10); + vt100.setCursor(0, 0); + // a wide character takes two cells, the second holding a null filler + vt100.appendString("한글"); + assertEqualsTerm("한 글 \n" + " \n" + " \n" + " ", toMultiLineText(term)); + assertEquals(4, vt100.getCursorColumn()); + vt100.setCursor(1, 0); + vt100.appendString("a한b"); + assertEqualsTerm("한 글 \n" + "a한 b \n" + " \n" + " ", toMultiLineText(term)); + assertEquals(4, vt100.getCursorColumn()); + // a character beyond the BMP is two chars in two cells + vt100.setCursor(2, 0); + vt100.appendString("a😀b"); + assertEquals(4, vt100.getCursorColumn()); + assertEquals("a😀b", new String(term.getChars(2), 0, 4)); + // a combining mark takes no cell + vt100.setCursor(3, 0); + vt100.appendString("e\u0301x"); + assertEquals(2, vt100.getCursorColumn()); + } + + @Test + public void testAppendStringWideAtMargin() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(4); + vt100.setDimensions(4, 5); + vt100.setCursor(0, 0); + vt100.appendString("abc한"); + assertEqualsTerm("abc한 \n" + " \n" + " \n" + " ", toMultiLineText(term)); + // a wide character is never split across the margin: it goes to the next line whole + vt100.setCursor(1, 0); + vt100.appendString("abcd한"); + assertEqualsTerm("abc한 \n" + "abcd \n" + "한 \n" + " ", toMultiLineText(term)); + assertTrue(term.isWrappedLine(1)); + assertEquals(2, vt100.getCursorLine()); + assertEquals(2, vt100.getCursorColumn()); + } + + @Test + public void testOverwriteHalfOfWide() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(4); + vt100.setDimensions(4, 10); + // writing over the glyph leaves its filler blank, and over the filler leaves the glyph blank + vt100.setCursor(0, 0); + vt100.appendString("한글"); + vt100.setCursor(0, 0); + vt100.appendString("x"); + assertEquals("x 글\000", new String(term.getChars(0), 0, 4)); + vt100.setCursor(1, 0); + vt100.appendString("한글"); + vt100.setCursor(1, 1); + vt100.appendString("x"); + assertEquals(" x글\000", new String(term.getChars(1), 0, 4)); + vt100.setCursor(2, 0); + vt100.appendString("한글"); + vt100.setCursor(2, 3); + vt100.appendString("나"); + assertEquals("한\000 나\000", new String(term.getChars(2), 0, 5)); + // narrow over narrow is untouched by any of this + vt100.setCursor(3, 0); + vt100.appendString("abcd"); + vt100.setCursor(3, 1); + vt100.appendString("XY"); + assertEquals("aXYd", new String(term.getChars(3), 0, 4)); + } + + @Test + public void testInsertModeWide() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(1); + vt100.setDimensions(1, 10); + vt100.setCursor(0, 0); + vt100.appendString("abcdef"); + vt100.setCursorColumn(1); + vt100.setInsertMode(true); + vt100.appendString("한"); + // pushes the rest along by two cells, not one + assertEquals("a한\000bcdef", new String(term.getChars(0), 0, 8)); + } } diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AllTestSuite.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AllTestSuite.java index 47660269f0a..236b1b61dc8 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AllTestSuite.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AllTestSuite.java @@ -21,6 +21,7 @@ */ @Suite @SelectClasses({ // + CharWidthTest.class, // SnapshotChangesTest.class, // SynchronizedTerminalTextDataTest.class, // TerminalTextDataFastScrollTest.class, // diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/CharWidthTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/CharWidthTest.java new file mode 100644 index 00000000000..dfe94127a8d --- /dev/null +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/CharWidthTest.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * All rights reserved. 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 + *******************************************************************************/ +package org.eclipse.terminal.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class CharWidthTest { + + @Test + public void testNarrow() { + assertEquals(1, CharWidth.of('A')); + assertEquals(1, CharWidth.of(0x00E9)); // e with acute + assertEquals(1, CharWidth.of(0x00B1)); // ambiguous: plus-minus + assertEquals(1, CharWidth.of(0x2500)); // ambiguous: box drawing + assertEquals(1, CharWidth.of(0xFFFD)); // ambiguous: replacement character + } + + @Test + public void testWide() { + assertEquals(2, CharWidth.of(0xAC00)); // hangul syllable + assertEquals(2, CharWidth.of(0x6F22)); // han ideograph + assertEquals(2, CharWidth.of(0xFF21)); // fullwidth A + assertEquals(2, CharWidth.of(0x1100)); // hangul jamo, leading consonant + assertEquals(2, CharWidth.of(0x3131)); // hangul compatibility jamo + assertEquals(2, CharWidth.of(0x3000)); // ideographic space + assertEquals(2, CharWidth.of(0x1F600)); // emoji + } + + @Test + public void testZeroWidth() { + assertEquals(0, CharWidth.of(0x0301)); // combining acute + assertEquals(0, CharWidth.of(0x200B)); // zero width space + assertEquals(0, CharWidth.of(0x1161)); // hangul jamo vowel, combines with the consonant before it + assertEquals(0, CharWidth.of('\n')); + assertEquals(0, CharWidth.of(0)); + assertEquals(0, CharWidth.of(0x7F)); + assertEquals(0, CharWidth.of(0x85)); // C1 control + } + + @Test + public void testOfString() { + assertEquals(0, CharWidth.ofString("")); + assertEquals(3, CharWidth.ofString("abc")); + assertEquals(7, CharWidth.ofString("한글abc")); // two hangul syllables, three letters + assertEquals(4, CharWidth.ofString("a😀b")); // surrogate pair counts once, as two cells + assertEquals(1, CharWidth.ofString("é")); // combining mark adds nothing + } + + @Test + public void testIsFiller() { + assertTrue(CharWidth.isFiller("가\000", 1)); + assertTrue(CharWidth.isFiller("😀\000", 2)); + assertFalse(CharWidth.isFiller("a\000", 1)); // an empty cell after a narrow character + assertFalse(CharWidth.isFiller("\000a", 0)); + assertFalse(CharWidth.isFiller("ab", 1)); + assertFalse(CharWidth.isFiller("가\000\000", 2)); // only the first null is the filler + } +} From a31a5444ae18582c1dc96a23d013c8854c6b07ec Mon Sep 17 00:00:00 2001 From: insjang Date: Wed, 2 Sep 2026 22:29:03 +0900 Subject: [PATCH 2/3] Terminal: keep the wrap mark honest, copy without the room a fold left, paint a selection whole, and keep scrolling while a drag goes past the edge The mark that says a line runs on to the next (set when the terminal folds a line at the margin) could only be set, never cleared. A program that draws its own screen writes the same row over and over, and a row that was folded in one frame is a row of its own in the next; the stale mark then joined unrelated rows when copying. The mark now comes off when a row is drawn over shorter than the margin, and when the line is erased to its end. ITerminalTextData gains clearWrappedLine, a default method (hence the version bump). Copying a folded line included the cells past its last character, which were never written to: on a line that ran on they are the room the fold left, on one that ended they are the rest of the screen. They are left out, so a folded line copies as the one line it is. The selection colour was drawn only where text was drawn, leaving the cells past the last character on a line and the gaps under narrow glyphs in the ordinary colours. The selected range on each line is now filled first. The canvas is created with NO_REDRAW_RESIZE, which repaints only what a resize uncovers; when the grid got narrower, what was drawn in the columns now past its edge stayed on screen until something else repainted it. A narrower grid now redraws. And a drag that goes past the top or bottom edge keeps scrolling while the pointer stays there, so a selection can reach beyond the visible page. Builds on the East Asian Width change for the filler handling in the copy path. --- .../META-INF/MANIFEST.MF | 2 +- .../emulator/VT100EmulatorBackend.java | 7 +++++ .../model/SynchronizedTerminalTextData.java | 5 ++++ .../internal/model/TerminalTextData.java | 5 ++++ .../model/TerminalTextDataFastScroll.java | 6 +++++ .../internal/model/TerminalTextDataStore.java | 5 ++++ .../model/TerminalTextDataWindow.java | 7 +++++ .../textcanvas/AbstractTextCanvasModel.java | 6 ++++- .../internal/textcanvas/TextCanvas.java | 27 +++++++++++++++++++ .../internal/textcanvas/TextLineRenderer.java | 10 +++++++ .../terminal/model/ITerminalTextData.java | 11 ++++++++ .../emulator/VT100EmulatorBackendTest.java | 24 +++++++++++++++++ .../model/AbstractITerminalTextDataTest.java | 3 +++ .../model/TerminalTextDataWindowTest.java | 4 +++ 14 files changed, 120 insertions(+), 2 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 0c618bbd154..2054f6b2dad 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.300.qualifier +Bundle-Version: 1.2.0.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 e4e13e270b5..96720f1cc37 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 @@ -217,6 +217,8 @@ public void eraseLineToEnd() { for (int col = fCursorColumn; col < fColumns; col++) { fTerminal.setChar(line, col, '\000', null); } + // Nothing is left out to the margin, so the line ends where the cursor is. + fTerminal.clearWrappedLine(line); } } @@ -374,6 +376,11 @@ public void appendString(String buffer) { line = doLineWrap(); } } else { + // Writing that stops short of the margin says the line ends here. + // A program that draws its own screen writes the same row again and + // again, and a row that was folded in one frame is a row of its own + // in the next, so the mark has to be able to come off. + fTerminal.clearWrappedLine(line); setCursorColumn(col); } } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/SynchronizedTerminalTextData.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/SynchronizedTerminalTextData.java index a142ef8578a..3de26a78cb4 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/SynchronizedTerminalTextData.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/SynchronizedTerminalTextData.java @@ -157,4 +157,9 @@ synchronized public boolean isWrappedLine(int line) { synchronized public void setWrappedLine(int line) { fData.setWrappedLine(line); } + + @Override + synchronized public void clearWrappedLine(int line) { + fData.clearWrappedLine(line); + } } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java index baa3b620df8..d24465d1b3a 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextData.java @@ -334,4 +334,9 @@ public boolean isWrappedLine(int line) { public void setWrappedLine(int line) { fData.setWrappedLine(line); } + + @Override + public void clearWrappedLine(int line) { + fData.clearWrappedLine(line); + } } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataFastScroll.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataFastScroll.java index c76fab20863..db68f309069 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataFastScroll.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataFastScroll.java @@ -308,4 +308,10 @@ public void setWrappedLine(int line) { fData.setWrappedLine(getPositionOfLine(line)); } + @Override + public void clearWrappedLine(int line) { + validateLineParameter(line); + fData.clearWrappedLine(getPositionOfLine(line)); + } + } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataStore.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataStore.java index 04763621546..d0f3ac2b398 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataStore.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataStore.java @@ -394,4 +394,9 @@ public boolean isWrappedLine(int line) { public void setWrappedLine(int line) { fWrappedLines.set(line); } + + @Override + public void clearWrappedLine(int line) { + fWrappedLines.clear(line); + } } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataWindow.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataWindow.java index 21e343ee6b8..92149232a88 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataWindow.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/model/TerminalTextDataWindow.java @@ -267,4 +267,11 @@ public void setWrappedLine(int line) { fData.setWrappedLine(line - fWindowStartLine); } } + + @Override + public void clearWrappedLine(int line) { + if (isInWindow(line)) { + fData.clearWrappedLine(line - fWindowStartLine); + } + } } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java index 09acdc6d71a..b0ea9837688 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/AbstractTextCanvasModel.java @@ -465,7 +465,11 @@ private String extractSelectedText() { } else { text = ""; //$NON-NLS-1$ } - buffer.append(text); + // Cells past the last character were never written to. They read as spaces + // because that is how an empty cell is drawn, but there is no text there to + // copy: on a line that ran on to the next one they are the room the fold + // left, and on one that ended they are the rest of the screen. + buffer.append(text.stripTrailing()); if (line < fSeletionEndLine && !fSelectionSnapshot.isWrappedLine(line)) { buffer.append('\n'); } diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java index 9d941c9099d..0af2a1d194a 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextCanvas.java @@ -58,6 +58,10 @@ public class TextCanvas extends GridCanvas { private final ILinelRenderer fCellRenderer; private boolean fScrollLock; private Point fDraggingStart; + /** -1 above the top, 1 below the bottom, 0 while the pointer is inside */ + private int fDragPast; + private boolean fDragScrolling; + private static final int DRAG_SCROLL_INTERVAL = 60; private Point fDraggingEnd; private boolean fHasSelection; private ResizeListener fResizeListener; @@ -270,6 +274,12 @@ public void mouseUp(MouseEvent e) { }); addMouseMoveListener(e -> { if (fDraggingStart != null) { + // Dragging past an edge keeps the lines beyond it coming into view. + fDragPast = e.y < 0 ? -1 : e.y >= getClientArea().height ? 1 : 0; + if (fDragPast != 0 && !fDragScrolling) { + fDragScrolling = true; + getDisplay().timerExec(DRAG_SCROLL_INTERVAL, this::dragScroll); + } Point curr = screenPointToCell(e.x, e.y); updateHasSelection(e); switch (fSelMode) { @@ -298,6 +308,18 @@ public void mouseUp(MouseEvent e) { setHorizontalBarVisible(false); } + private void dragScroll() { + if (isDisposed() || fDraggingStart == null || fDragPast == 0) { + fDragScrolling = false; + return; + } + scrollYDelta(fDragPast * getCellHeight()); + Point p = toControl(getDisplay().getCursorLocation()); + setSelection(screenPointToCell(p.x, p.y)); + redraw(); + getDisplay().timerExec(DRAG_SCROLL_INTERVAL, this::dragScroll); + } + private static class Range { final Point start; final Point end; @@ -495,6 +517,11 @@ private void calculateGrid() { } finally { setRedraw(true); } + // NO_REDRAW_RESIZE paints only what a resize uncovers. When the grid gets + // narrower, what was drawn in the columns now past its edge stays there. + if (getVirtualBounds().width < virtualBounds.width) { + redraw(); + } } void scrollToEnd() { diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java index a225b8486d2..c24a35857f1 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/internal/textcanvas/TextLineRenderer.java @@ -96,6 +96,16 @@ public void drawLine(ITextCanvasModel model, GC gc, int line, int x, int y, int setupGC(gc, style); Point start = model.getSelectionStart(); Point end = model.getSelectionEnd(); + // Everything the selection covers on this line goes down first. Drawing + // it as the text is drawn leaves out whatever the text does not reach: + // the cells past the last character a program put on the line, and the + // cells where the font draws a glyph narrower than the one it sits in. + int from = Math.max(start.y == line ? start.x : 0, colFirst); + int to = end.y == line ? Math.min(end.x + 1, colLast) : colLast; + if (to > from) { + gc.fillRectangle(x + (from - colFirst) * getCellWidth(), y, (to - from) * getCellWidth(), + getCellHeight()); + } char[] chars = model.getTerminalText().getChars(line); if (chars != null) { int offset = 0; diff --git a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/model/ITerminalTextData.java b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/model/ITerminalTextData.java index f972bbbe28b..ce0a1d3c4cf 100644 --- a/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/model/ITerminalTextData.java +++ b/terminal/bundles/org.eclipse.terminal.control/src/org/eclipse/terminal/model/ITerminalTextData.java @@ -156,4 +156,15 @@ public interface ITerminalTextData extends ITerminalTextDataReadOnly { */ void setWrappedLine(int line); + /** + * Says the line ends where it is drawn, undoing {@link #setWrappedLine(int)}. + * A program that draws its own screen writes a line over and over, and what ran + * on in one frame may end in the next. + * + * @param line line to mark, must be >=0 and < {@link #getHeight()} + * @since 1.2 + */ + default void clearWrappedLine(int line) { + } + } 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 c53ea81f425..ff9736d39bf 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 @@ -1175,4 +1175,28 @@ public void testInsertModeWide() { // pushes the rest along by two cells, not one assertEquals("a한\000bcdef", new String(term.getChars(0), 0, 8)); } + + @Test + public void testWrappedLineMarkComesOff() { + ITerminalTextData term = makeITerminalTextData(); + IVT100EmulatorBackend vt100 = makeBakend(term); + term.setMaxHeight(3); + vt100.setDimensions(3, 5); + vt100.setCursor(0, 0); + // a line the terminal folded is marked as running on to the next + vt100.appendString("abcdefg"); + assertTrue(term.isWrappedLine(0)); + assertFalse(term.isWrappedLine(1)); + // drawn over shorter, the line ends there and the mark comes off + vt100.setCursor(0, 0); + vt100.appendString("xyz"); + assertFalse(term.isWrappedLine(0)); + // erasing to the end of the line says the same + vt100.setCursor(0, 0); + vt100.appendString("abcdefg"); + assertTrue(term.isWrappedLine(0)); + vt100.setCursor(0, 3); + vt100.eraseLineToEnd(); + assertFalse(term.isWrappedLine(0)); + } } diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AbstractITerminalTextDataTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AbstractITerminalTextDataTest.java index f1d3857ba27..457b4ebc943 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AbstractITerminalTextDataTest.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/AbstractITerminalTextDataTest.java @@ -654,5 +654,8 @@ public void testWrappedLines() { assertTrue(term.isWrappedLine(3)); term.cleanLine(0); assertFalse(term.isWrappedLine(0)); + term.setWrappedLine(2); + term.clearWrappedLine(2); + assertFalse(term.isWrappedLine(2)); } } diff --git a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/TerminalTextDataWindowTest.java b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/TerminalTextDataWindowTest.java index 163af3be430..b4be30273ad 100644 --- a/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/TerminalTextDataWindowTest.java +++ b/terminal/tests/org.eclipse.terminal.test/src/org/eclipse/terminal/internal/model/TerminalTextDataWindowTest.java @@ -441,5 +441,9 @@ public void testWrappedLines() { assertTrue(term.isWrappedLine(3)); term.cleanLine(3); assertFalse(term.isWrappedLine(3)); + term.setWrappedLine(3); + term.clearWrappedLine(3); + assertFalse(term.isWrappedLine(3)); + term.clearWrappedLine(0); // outside window, harmless } } From 1a7bc9373682593d8dd94a1e35cd44396ee676e8 Mon Sep 17 00:00:00 2001 From: insjang Date: Thu, 3 Sep 2026 12:33:34 +0900 Subject: [PATCH 3/3] Terminal: bump org.eclipse.terminal.model package export to 1.1.0 New @since 1.2 API was added to the package without bumping its Export-Package version, which API Tools flags as an error. --- .../bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2054f6b2dad..8ef0e9347c0 100644 --- a/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF +++ b/terminal/bundles/org.eclipse.terminal.control/META-INF/MANIFEST.MF @@ -30,5 +30,5 @@ Export-Package: org.eclipse.terminal.connector;version="1.0.100"; org.eclipse.terminal.internal.model;x-internal:=true, org.eclipse.terminal.internal.preferences;x-internal:=true;x-friends:="org.eclipse.terminal.view.ui", org.eclipse.terminal.internal.textcanvas;x-internal:=true, - org.eclipse.terminal.model;version="1.0.100";uses:="org.eclipse.swt.graphics" + org.eclipse.terminal.model;version="1.1.0";uses:="org.eclipse.swt.graphics" Automatic-Module-Name: org.eclipse.terminal.control