diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 8d36fb21..afc41a3a 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -126,10 +126,6 @@ export class InputHandler extends Disposable implements IInputHandler { protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; - // Cached result of getJoinProperties(..., precedingCodepoint). - // Only valid if precedingCodepoint !== 0 - public precedingJoinState: number = -1; // UnicodeJoinProperties - private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); public getAttrData(): IAttributeData { return this._curAttrData; } private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); @@ -521,8 +517,7 @@ export class InputHandler extends Disposable implements IInputHandler { bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - let precedingJoinState = this._parser.precedingCodepoint === 0 ? 0 - : this.precedingJoinState; + let precedingJoinState = this._parser.precedingJoinState; for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -636,18 +631,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } - this.precedingJoinState = precedingJoinState; - // store last char in Parser.precedingCodepoint for REP to work correctly - // This needs to check whether: - // - 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.isCombined()) { - this._parser.precedingCodepoint = this._workCell.getChars().charCodeAt(0); - } else { - this._parser.precedingCodepoint = this._workCell.content; - } - } + this._parser.precedingJoinState = precedingJoinState; // 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)) { @@ -1593,9 +1577,8 @@ export class InputHandler extends Disposable implements IInputHandler { * If the character preceding REP is a control function or part of a control function, * the effect of REP is not defined by this Standard. * - * Since we propagate the terminal as xterm-256color we have to follow xterm's behavior: - * - fullwidth + surrogate chars are ignored - * - for combining chars only the base char gets repeated + * We extend xterm's behavior to allow repeating entire grapheme clusters. + * This isn't 100% xterm-compatible, but it seems saner and more useful. * - text attrs are applied normally * - wrap around is respected * - any valid sequence resets the carried forward char @@ -1609,16 +1592,29 @@ export class InputHandler extends Disposable implements IInputHandler { * (NOOP for any other sequence in between or NON ASCII characters). */ public repeatPrecedingCharacter(params: IParams): boolean { - if (!this._parser.precedingCodepoint) { + const joinState = this._parser.precedingJoinState; + if (!joinState) { return true; } // call print to insert the chars and handle correct wrapping const length = params.params[0] || 1; - const data = new Uint32Array(length); - for (let i = 0; i < length; ++i) { - data[i] = this._parser.precedingCodepoint; + const chWidth = UnicodeService.extractWidth(joinState); + const x = this._activeBuffer.x - chWidth; + const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + const text = bufferRow.getString(x); + const data = new Uint32Array(text.length * length); + let idata = 0; + for (let itext = 0; itext < text.length; ) { + const ch = text.codePointAt(itext) || 0; + data[idata++] = ch; + itext += ch > 0xffff ? 2 : 1; } - this.print(data, 0, data.length); + let tlength = idata; + for (let i = 1; i < length; ++i) { + data.copyWithin(tlength, 0, idata); + tlength += idata; + } + this.print(data, 0, tlength); return true; } diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index de206322..b3d02768 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -230,7 +230,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser { public initialState: number; public currentState: number; - public precedingCodepoint: number; + public precedingJoinState: number; // UnicodeJoinProperties // buffers over several parse calls protected _params: Params; @@ -271,7 +271,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params = new Params(); // defaults to 32 storable params/subparams this._params.addParam(0); // ZDM this._collect = 0; - this.precedingCodepoint = 0; + this.precedingJoinState = 0; // set default fallback handlers and handler lookup containers this._printHandlerFb = (data, start, end): void => { }; @@ -448,7 +448,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; - this.precedingCodepoint = 0; + this.precedingJoinState = 0; // abort pending continuation from async handler // Here the RESET type indicates, that the next parse call will // ignore any saved stack, instead continues sync with next codepoint from GROUND @@ -610,7 +610,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // cleanup before continuing with the main sync loop this._parseStack.state = ParserStackType.NONE; start = this._parseStack.chunkPos + 1; - this.precedingCodepoint = 0; + this.precedingJoinState = 0; this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK; } } @@ -653,7 +653,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.EXECUTE: if (this._executeHandlers[code]) this._executeHandlers[code](); else this._executeHandlerFb(code); - this.precedingCodepoint = 0; + this.precedingJoinState = 0; break; case ParserAction.IGNORE: break; @@ -688,7 +688,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (j < 0) { this._csiHandlerFb(this._collect << 8 | code, this._params); } - this.precedingCodepoint = 0; + this.precedingJoinState = 0; break; case ParserAction.PARAM: // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a) @@ -727,7 +727,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP if (jj < 0) { this._escHandlerFb(this._collect << 8 | code); } - this.precedingCodepoint = 0; + this.precedingJoinState = 0; break; case ParserAction.CLEAR: this._params.reset(); @@ -758,7 +758,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; - this.precedingCodepoint = 0; + this.precedingJoinState = 0; break; case ParserAction.OSC_START: this._oscParser.start(); @@ -783,7 +783,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.reset(); this._params.addParam(0); // ZDM this._collect = 0; - this.precedingCodepoint = 0; + this.precedingJoinState = 0; break; } this.currentState = transition & TableAccess.TRANSITION_STATE_MASK; diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index a1ea0ec2..839a0a72 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -146,11 +146,12 @@ export type PrintFallbackHandlerType = PrintHandlerType; */ export interface IEscapeSequenceParser extends IDisposable { /** - * Preceding codepoint to get REP working correctly. - * This must be set by the print handler as last action. - * It gets reset by the parser for any valid sequence beside REP itself. + * Preceding grapheme-join-state. + * Used for joining graphame clusters across calls to `print`. + * Also used by REP to check if repeat a character is allowed. + * It gets reset by the parser for any valid sequence besides text. */ - precedingCodepoint: number; + precedingJoinState: number; // More specifically: UnicodeJoinProperties /** * Reset the parser to its initial state (handlers are kept). diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index c894a4d8..b8d20455 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -272,18 +272,18 @@ describe('InputHandler Integration Tests', function(): void { `); await pollFor(page, () => getLinesAsArray(4), ['##', '##', '##', '######']); await pollFor(page, () => getCursor(), { col: 6, row: 3 }); - // should not repeat on fullwidth chars + // do repeat on fullwidth chars (change from xterm) await page.evaluate(` window.term.reset(); window.term.write('¥\x1b[10b'); `); - await pollFor(page, () => getLinesAsArray(1), ['¥']); - // should repeat only base char of combining + await pollFor(page, () => getLinesAsArray(1), ['¥¥¥¥¥¥¥¥¥¥¥']); + // change from xterm: repeat grapheme cluster await page.evaluate(` window.term.reset(); - window.term.write('e\u0301\x1b[5b'); + window.term.write('e\u0301\x1b[2b'); `); - await pollFor(page, () => getLinesAsArray(1), ['e\u0301eeeee']); + await pollFor(page, () => getLinesAsArray(1), ['e\u0301e\u0301e\u0301']); // should wrap correctly await page.evaluate(` window.term.reset();