Repository: microsoft/vscode (TypeScript)
Date: 2022-12-16 22:10:31+00:00
Task type: Bug Fix

Issue:
Words in the terminal containing emoji are longer than they're meant to be
While ctrl is held, the underlined word in `echo "A🙂 http://"` should not include the trailing space:

![image](https://user-images.githubusercontent.com/2193314/208437819-0db6912b-7e83-4a9b-a81d-033f4ce8f8e9.png)




Issue discussion:

Solution patch:
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts
@@ -38,7 +38,7 @@ export function convertLinkRangeToBuffer(
 	let startOffset = 0;
 	const startWrappedLineCount = Math.ceil(range.startColumn / bufferWidth);
 	for (let y = 0; y < Math.min(startWrappedLineCount); y++) {
-		const lineLength = Math.min(bufferWidth, range.startColumn - y * bufferWidth);
+		const lineLength = Math.min(bufferWidth, (range.startColumn - 1) - y * bufferWidth);
 		let lineOffset = 0;
 		const line = lines[y];
 		// Sanity check for line, apparently this can happen but it's not clear under what
@@ -65,9 +65,8 @@ export function convertLinkRangeToBuffer(
 	let endOffset = 0;
 	const endWrappedLineCount = Math.ceil(range.endColumn / bufferWidth);
 	for (let y = Math.max(0, startWrappedLineCount - 1); y < endWrappedLineCount; y++) {
-		const start = (y === startWrappedLineCount - 1 ? (range.startColumn + startOffset) % bufferWidth : 0);
+		const start = (y === startWrappedLineCount - 1 ? (range.startColumn - 1 + startOffset) % bufferWidth : 0);
 		const lineLength = Math.min(bufferWidth, range.endColumn + startOffset - y * bufferWidth);
-		const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0);
 		let lineOffset = 0;
 		const line = lines[y];
 		// Sanity check for line, apparently this can happen but it's not clear under what
@@ -76,22 +75,27 @@ export function convertLinkRangeToBuffer(
 		if (!line) {
 			break;
 		}
-		for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) {
+		for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset); x++) {
 			const cell = line.getCell(x);
 			// This is unexpected but it means the character doesn't exist, so we shouldn't add to
 			// the offset
 			if (!cell) {
 				break;
 			}
-			// Offset for 0 cells following wide characters
 			const width = cell.getWidth();
+			const chars = cell.getChars();
+			// Offset for null cells following wide characters
 			if (width === 2) {
 				lineOffset++;
 			}
 			// Offset for early wrapping when the last cell in row is a wide character
-			if (x === bufferWidth - 1 && cell.getChars() === '') {
+			if (x === bufferWidth - 1 && chars === '') {
 				lineOffset++;
 			}
+			// Offset multi-code characters like emoji
+			if (chars.length > 1) {
+				lineOffset -= chars.length - 1;
+			}
 		}
 		endOffset += lineOffset;
 	}


Test patch:
diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkHelpers.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkHelpers.test.ts
--- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkHelpers.test.ts
+++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkHelpers.test.ts
@@ -31,6 +31,16 @@ suite('Workbench - Terminal Link Helpers', () => {
 				end: { x: 7 + 1, y: 2 }
 			});
 		});
+		test('should give correct range for links containing multi-character emoji', () => {
+			const lines = createBufferLineArray([
+				{ text: 'A🙂 http://', width: 11 }
+			]);
+			const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 0 + 1, startLineNumber: 1, endColumn: 2 + 1, endLineNumber: 1 }, 0);
+			assert.deepStrictEqual(bufferRange, {
+				start: { x: 1, y: 1 },
+				end: { x: 2, y: 1 }
+			});
+		});
 		test('should convert ranges for combining characters before the link', () => {
 			const lines = createBufferLineArray([
 				{ text: 'A🙂 http://', width: 11 },
@@ -38,8 +48,8 @@ suite('Workbench - Terminal Link Helpers', () => {
 			]);
 			const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4 + 1, startLineNumber: 1, endColumn: 19 + 1, endLineNumber: 1 }, 0);
 			assert.deepStrictEqual(bufferRange, {
-				start: { x: 4, y: 1 },
-				end: { x: 7, y: 2 }
+				start: { x: 6, y: 1 },
+				end: { x: 9, y: 2 }
 			});
 		});
 		test('should convert ranges for wide characters inside the link', () => {
@@ -64,15 +74,15 @@ suite('Workbench - Terminal Link Helpers', () => {
 				end: { x: 7 + 2, y: 2 }
 			});
 		});
-		test('should convert ranges for emoji before before and wide inside the link', () => {
+		test('should convert ranges for emoji before and wide inside the link', () => {
 			const lines = createBufferLineArray([
 				{ text: 'A🙂 http://', width: 11 },
 				{ text: 't.com/文/', width: 9 }
 			]);
 			const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4 + 1, startLineNumber: 1, endColumn: 19 + 1, endLineNumber: 1 }, 0);
 			assert.deepStrictEqual(bufferRange, {
-				start: { x: 4, y: 1 },
-				end: { x: 7 + 1, y: 2 }
+				start: { x: 6, y: 1 },
+				end: { x: 10 + 1, y: 2 }
 			});
 		});
 		test('should convert ranges for ascii characters (link starts on wrapped)', () => {
@@ -99,6 +109,38 @@ suite('Workbench - Terminal Link Helpers', () => {
 				end: { x: 7 + 1, y: 3 }
 			});
 		});
+		test('regression test #147619: 获取模板 25235168 的预览图失败', () => {
+			const lines = createBufferLineArray([
+				{ text: '获取模板 25235168 的预览图失败', width: 30 }
+			]);
+			assert.deepStrictEqual(convertLinkRangeToBuffer(lines, 30, {
+				startColumn: 1,
+				startLineNumber: 1,
+				endColumn: 5,
+				endLineNumber: 1
+			}, 0), {
+				start: { x: 1, y: 1 },
+				end: { x: 8, y: 1 }
+			});
+			assert.deepStrictEqual(convertLinkRangeToBuffer(lines, 30, {
+				startColumn: 6,
+				startLineNumber: 1,
+				endColumn: 14,
+				endLineNumber: 1
+			}, 0), {
+				start: { x: 10, y: 1 },
+				end: { x: 17, y: 1 }
+			});
+			assert.deepStrictEqual(convertLinkRangeToBuffer(lines, 30, {
+				startColumn: 15,
+				startLineNumber: 1,
+				endColumn: 21,
+				endLineNumber: 1
+			}, 0), {
+				start: { x: 19, y: 1 },
+				end: { x: 30, y: 1 }
+			});
+		});
 		test('should convert ranges for wide characters inside the link (link starts on wrapped)', () => {
 			const lines = createBufferLineArray([
 				{ text: 'AAAAAAAAAAA', width: 11 },
@@ -111,7 +153,7 @@ suite('Workbench - Terminal Link Helpers', () => {
 				end: { x: 7 + 1, y: 3 }
 			});
 		});
-		test('should convert ranges for wide characters before and inside the link', () => {
+		test('should convert ranges for wide characters before and inside the link #2', () => {
 			const lines = createBufferLineArray([
 				{ text: 'AAAAAAAAAAA', width: 11 },
 				{ text: 'A文 http://', width: 11 },
@@ -130,10 +172,10 @@ suite('Workbench - Terminal Link Helpers', () => {
 				{ text: '://t.com/f/', width: 11 }
 			]);
 			const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
-			// This test ensures that the start offset is applies to the end before it's counted
+			// This test ensures that the start offset is applied to the end before it's counted
 			assert.deepStrictEqual(bufferRange, {
-				start: { x: 4 + 4, y: 2 },
-				end: { x: 7 + 4, y: 3 }
+				start: { x: 3 + 4, y: 2 },
+				end: { x: 6 + 4, y: 3 }
 			});
 		});
 		test('should convert ranges for several wide characters before and inside the link', () => {
@@ -143,11 +185,11 @@ suite('Workbench - Terminal Link Helpers', () => {
 				{ text: '://t.com/文', width: 11 },
 				{ text: '文/', width: 3 }
 			]);
-			const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 31, endLineNumber: 1 }, 0);
+			const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 14, startLineNumber: 1, endColumn: 31, endLineNumber: 1 }, 0);
 			// This test ensures that the start offset is applies to the end before it's counted
 			assert.deepStrictEqual(bufferRange, {
-				start: { x: 4 + 4, y: 2 },
-				end: { x: 2, y: 4 }
+				start: { x: 5, y: 2 },
+				end: { x: 1, y: 4 }
 			});
 		});
 	});
@@ -188,7 +230,7 @@ class TestBufferLine implements IBufferLine {
 				char += '\ude42';
 			}
 			cells.push(char);
-			if (this._text.charAt(i) === TEST_WIDE_CHAR) {
+			if (this._text.charAt(i) === TEST_WIDE_CHAR || char.charCodeAt(0) > 255) {
 				// Skip the next character as it's width is 0
 				cells.push(TEST_NULL_CHAR);
 				wideNullCellOffset++;
@@ -202,7 +244,13 @@ class TestBufferLine implements IBufferLine {
 				switch (cells[x]) {
 					case TEST_WIDE_CHAR: return 2;
 					case TEST_NULL_CHAR: return 0;
-					default: return 1;
+					default: {
+						// Naive measurement, assume anything our of ascii in tests are wide
+						if (cells[x].charCodeAt(0) > 255) {
+							return 2;
+						}
+						return 1;
+					}
 				}
 			}
 		} as any;