Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.0.qualifier
Bundle-Activator: org.eclipse.terminal.internal.control.impl.TerminalPlugin
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -216,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);
}
}

Expand Down Expand Up @@ -311,18 +314,58 @@ 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;
while (i < chars.length) {
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) {
Expand All @@ -333,12 +376,56 @@ 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);
}
}
}
}

/**
* 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 <code>offset</code> that each
* occupy exactly one cell, so that they can be copied in one block. Capped at
* <code>max</code> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -427,8 +428,17 @@ private static String scrubLine(String text) {
}
text = text.substring(0, i + 1);
// </J2ME-CDC-1.1 version>
// 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();
}

/**
Expand All @@ -455,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');
}
Expand Down
Loading
Loading