diff --git a/addons/xterm-addon-unicode-graphemes/src/UnicodeGraphemeProvider.ts b/addons/xterm-addon-unicode-graphemes/src/UnicodeGraphemeProvider.ts index 69f113fb..3dd8af9c 100644 --- a/addons/xterm-addon-unicode-graphemes/src/UnicodeGraphemeProvider.ts +++ b/addons/xterm-addon-unicode-graphemes/src/UnicodeGraphemeProvider.ts @@ -10,7 +10,7 @@ import * as UC from './UnicodeProperties'; export class UnicodeGraphemeProvider implements IUnicodeVersionProvider { public readonly version = '15-graphemes'; - + public ambiguousCharsAreWide: boolean = false; constructor() { } @@ -19,9 +19,8 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider { let w = UC.infoToWidthInfo(charInfo); let shouldJoin = false; if (w >= 2) { - const preferWide = false; //this.ambiguousCharsAreWide(context); // Treat emoji_presentation_selector as WIDE. - w = w == 3 || preferWide || codepoint === 0xfe0f ? 2 : 1; + w = w == 3 || this.ambiguousCharsAreWide || codepoint === 0xfe0f ? 2 : 1; } else w = 1; if (preceding !== 0) { @@ -31,14 +30,23 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider { if (shouldJoin) { if (oldWidth > w) w = oldWidth; - else if (charInfo === 32) // FIXME UC.GRAPHEME_BREAK_SAW_Regional_Pair) + else if (charInfo === 32) // FIXME UC.GRAPHEME_BREAK_SAW_Regional_Pair) w = 2; } } return UnicodeService.createPropertyValue(charInfo, w, shouldJoin); } - public wcwidth(num: number): UnicodeCharWidth { - return UC.infoToWidth(UC.getInfo(num)); + public wcwidth(codepoint: number): UnicodeCharWidth { + let charInfo = UC.getInfo(codepoint); + let w = UC.infoToWidthInfo(charInfo); + let kind = (charInfo & UC.GRAPHEME_BREAK_MASK) >> UC.GRAPHEME_BREAK_SHIFT; + if (kind === UC.GRAPHEME_BREAK_Extend + || kind === UC.GRAPHEME_BREAK_Prepend) + return 0; + else if (w >= 2) + return w == 3 || this.ambiguousCharsAreWide? 2 : 1; + else + return 1; } } diff --git a/addons/xterm-addon-unicode-graphemes/test/UnicodeGraphemesAddon.api.ts b/addons/xterm-addon-unicode-graphemes/test/UnicodeGraphemesAddon.api.ts new file mode 100644 index 00000000..f00e023d --- /dev/null +++ b/addons/xterm-addon-unicode-graphemes/test/UnicodeGraphemesAddon.api.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; +import { Browser, Page } from 'playwright'; + +const APP = 'http://127.0.0.1:3001/test'; + +let browser: Browser; +let page: Page; +const width = 800; +const height = 600; + +describe('UnicodeGraphemesAddon', () => { + before(async function(): Promise { + browser = await launchBrowser(); + page = await (await browser.newContext()).newPage(); + await page.setViewportSize({ width, height }); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async function(): Promise { + await page.goto(APP); + await openTerminal(page); + }); + const ourVersion = '15-graphemes'; + it('wcwidth V15 emoji test', async () => { + await page.evaluate(` + window.unicode = new UnicodeGraphemesAddon(); + window.term.loadAddon(window.unicode); + `); + // should have loaded '15-graphemes' + assert.deepEqual(await page.evaluate(`window.term.unicode.versions`), ['6', ourVersion]); + // switch should not throw + await page.evaluate(`window.term.unicode.activeVersion = '${ourVersion}';`); + assert.deepEqual(await page.evaluate(`window.term.unicode.activeVersion`), ourVersion); + // v6: 10, V15: 20 + assert.deepEqual(await page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20); + }); +}); diff --git a/addons/xterm-addon-unicode-graphemes/test/tsconfig.json b/addons/xterm-addon-unicode-graphemes/test/tsconfig.json new file mode 100644 index 00000000..4b3cb31c --- /dev/null +++ b/addons/xterm-addon-unicode-graphemes/test/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out-test", + "sourceMap": true, + "removeComments": true, + "strict": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ] + }, + "types": [ + "../../../node_modules/@types/mocha", + "../../../node_modules/@types/node", + "../../../out-test/api/TestUtils" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + } + ] +} diff --git a/addons/xterm-addon-unicode-graphemes/typings/xterm-addon-unicode11.d.ts b/addons/xterm-addon-unicode-graphemes/typings/xterm-addon-unicode-graphemes.d.ts similarity index 67% rename from addons/xterm-addon-unicode-graphemes/typings/xterm-addon-unicode11.d.ts rename to addons/xterm-addon-unicode-graphemes/typings/xterm-addon-unicode-graphemes.d.ts index 1d0dce1b..e4a33350 100644 --- a/addons/xterm-addon-unicode-graphemes/typings/xterm-addon-unicode11.d.ts +++ b/addons/xterm-addon-unicode-graphemes/typings/xterm-addon-unicode-graphemes.d.ts @@ -1,11 +1,11 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2023 The xterm.js authors. All rights reserved. * @license MIT */ import { Terminal, ITerminalAddon } from 'xterm'; -declare module 'xterm-addon-unicode11' { +declare module 'xterm-addon-unicode-graphemes' { export class Unicode11Addon implements ITerminalAddon { constructor(); public activate(terminal: Terminal): void; diff --git a/addons/xterm-addon-unicode11/src/UnicodeV11.ts b/addons/xterm-addon-unicode11/src/UnicodeV11.ts index c58e2fde..c1ef08c1 100644 --- a/addons/xterm-addon-unicode11/src/UnicodeV11.ts +++ b/addons/xterm-addon-unicode11/src/UnicodeV11.ts @@ -219,14 +219,12 @@ export class UnicodeV11 implements IUnicodeVersionProvider { return 1; } - charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { + public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { let width = this.wcwidth(codepoint); - let shouldJoin = width === 0; + let shouldJoin = width === 0 && preceding !== 0; if (shouldJoin) { - let oldWidth = preceding === 0 ? 0 - : UnicodeService.extractWidth(preceding); + const oldWidth = UnicodeService.extractWidth(preceding); if (oldWidth === 0) { - width = 1; shouldJoin = false; } else if (oldWidth > width) { width = oldWidth; diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 4412a3ae..baabd82f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -530,16 +530,15 @@ export class InputHandler extends Disposable implements IInputHandler { } } - let precedingInfo = this._parser.precedingCodepoint === 0 ? 0 + const precedingInfo = this._parser.precedingCodepoint === 0 ? 0 : this._parser.precedingJoinState; - // calculate print space - // expensive call, therefore we save width in line buffer - let currentInfo = this._unicodeService.charProperties(code, precedingInfo); - let chWidth = UnicodeService.extractWidth(currentInfo); - let shouldJoin = UnicodeService.extractShouldJoin(currentInfo); + const currentInfo = this._unicodeService.charProperties(code, precedingInfo); + chWidth = UnicodeService.extractWidth(currentInfo); + const shouldJoin = UnicodeService.extractShouldJoin(currentInfo); const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingInfo) : 0; this._parser.precedingCodepoint = code; this._parser.precedingJoinState = currentInfo; + if (screenReaderMode) { this._onA11yChar.fire(stringFromCodePoint(code)); } @@ -575,7 +574,7 @@ export class InputHandler extends Disposable implements IInputHandler { // Combining character widens 1 column to 2. // Move old character to next line. bufferRow.copyCellsFrom(oldRow as BufferLine, - oldCol, 0, oldWidth, false); + oldCol, 0, oldWidth, false); } // clear left over cells to the right while (oldCol < cols) { @@ -596,12 +595,12 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for this._activeBuffer.x to avoid overflow left if (shouldJoin && this._activeBuffer.x) { - const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2 + const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2; // if empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars bufferRow.addCodepointToCell(this._activeBuffer.x - offset, - code, chWidth); + code, chWidth); this._activeBuffer.x += chWidth - oldWidth; continue; } @@ -609,7 +608,7 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(this._activeBuffer.x, chWidth, this._activeBuffer.getNullCell(curAttr), curAttr); + bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr), curAttr); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell @@ -631,22 +630,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } } - /* - // store last char in Parser.precedingCodepoint for REP to work correctly - // This needs to check whether: - // - fullwidth + surrogates: reset - // - combining: only base char gets carried on (bug in xterm?) - if (end - start > 0) { - bufferRow.loadCell(this._activeBuffer.x - 1, this._workCell); - if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { - this._parser.precedingCodepoint = 0; - } else if (this._workCell.isCombined()) { - this._parser.precedingCodepoint = this._workCell.getChars().charCodeAt(0); - } else { - this._parser.precedingCodepoint = this._workCell.content; - } - } - */ + // handle wide chars: reset cell to the right if it is second cell of a wide char if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) { bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 51346583..ceb0dc29 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -170,9 +170,17 @@ export class MockUnicodeService implements IUnicodeService { public onChange: IEvent = new EventEmitter().event; public wcwidth = (codepoint: number): UnicodeCharWidth => this._provider.wcwidth(codepoint); public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { - const w = this.wcwidth(codepoint); - const shouldJoin = w !== 0; - return UnicodeService.createPropertyValue(0, w, shouldJoin); + let width = this.wcwidth(codepoint); + let shouldJoin = width === 0 && preceding !== 0; + if (shouldJoin) { + const oldWidth = UnicodeService.extractWidth(preceding); + if (oldWidth === 0) { + shouldJoin = false; + } else if (oldWidth > width) { + width = oldWidth; + } + } + return UnicodeService.createPropertyValue(0, width, shouldJoin); } public getStringCellWidth(s: string): number { throw new Error('Method not implemented.'); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6059d2c5..d4a9adcd 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -227,7 +227,7 @@ export class BufferLine implements IBufferLine { * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ - public addCodepointToCell(index: number, codePoint: number, width: number): void { + public addCodepointToCell(index: number, codePoint: number, width: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED_MASK) { // we already have a combined string, simply add diff --git a/src/common/input/UnicodeV6.ts b/src/common/input/UnicodeV6.ts index 352d9920..83265f70 100644 --- a/src/common/input/UnicodeV6.ts +++ b/src/common/input/UnicodeV6.ts @@ -2,8 +2,7 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { IUnicodeVersionProvider } from 'common/services/Services'; -import { UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services'; +import { IUnicodeVersionProvider, UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services'; import { UnicodeService } from 'common/services/UnicodeService'; const BMP_COMBINING = [ @@ -130,14 +129,12 @@ export class UnicodeV6 implements IUnicodeVersionProvider { return 1; } - charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { + public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { let width = this.wcwidth(codepoint); - let shouldJoin = width === 0; + let shouldJoin = width === 0 && preceding !== 0; if (shouldJoin) { - let oldWidth = preceding === 0 ? 0 - : UnicodeService.extractWidth(preceding); + const oldWidth = UnicodeService.extractWidth(preceding); if (oldWidth === 0) { - width = 1; shouldJoin = false; } else if (oldWidth > width) { width = oldWidth; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index d9785d35..3e5900e1 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -296,7 +296,6 @@ export interface IOscLinkService { /** Width and Grapheme_Cluster_Break properties of a character. */ export type UnicodeCharProperties = number; -export const UnicodeInitialProperties: UnicodeCharProperties = 0; // UNEEDED? export type UnicodeCharWidth = 0 | 1 | 2; export const IUnicodeService = createDecorator('UnicodeService'); diff --git a/src/common/services/UnicodeService.test.ts b/src/common/services/UnicodeService.test.ts index 110e725c..01e3c086 100644 --- a/src/common/services/UnicodeService.test.ts +++ b/src/common/services/UnicodeService.test.ts @@ -12,7 +12,7 @@ class DummyProvider implements IUnicodeVersionProvider { public wcwidth(n: number): 0 | 1 | 2 { return 2; } - charProperties(codepoint: number): number { + public charProperties(codepoint: number): number { return UnicodeService.createPropertyValue(0, this.wcwidth(codepoint)); } } diff --git a/src/common/services/UnicodeService.ts b/src/common/services/UnicodeService.ts index da0eae11..e38eb2a7 100644 --- a/src/common/services/UnicodeService.ts +++ b/src/common/services/UnicodeService.ts @@ -70,6 +70,7 @@ export class UnicodeService implements IUnicodeService { public getStringCellWidth(s: string): number { let result = 0; + let precedingInfo = 0; const length = s.length; for (let i = 0; i < length; ++i) { let code = s.charCodeAt(i); @@ -92,12 +93,18 @@ export class UnicodeService implements IUnicodeService { result += this.wcwidth(second); } } - result += this.wcwidth(code); + const currentInfo = this.charProperties(code, precedingInfo); + let chWidth = UnicodeService.extractWidth(currentInfo); + if (UnicodeService.extractShouldJoin(currentInfo)) { + chWidth -= UnicodeService.extractWidth(precedingInfo); + } + result += chWidth; + precedingInfo = currentInfo; } return result; } - charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { + public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties { return this._activeProvider.charProperties(codepoint, preceding); } }