/** * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { stringFromCodePoint } from './core/input/TextDecoder'; /** * buffer memory layout: * * | uint32_t | uint32_t | uint32_t | * | `content` | `FG` | `BG` | * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | */ /** typed array slots taken by one cell */ const CELL_SIZE = 3; /** * Cell member indices. * * Direct access: * `content = data[column * CELL_SIZE + Cell.CONTENT];` * `fg = data[column * CELL_SIZE + Cell.FG];` * `bg = data[column * CELL_SIZE + Cell.BG];` */ const enum Cell { CONTENT = 0, FG = 1, // currently simply holds all known attrs BG = 2 // currently unused } /** * Bitmasks and helper for accessing data in `content`. */ const enum Content { /** * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) * read: `codepoint = content & Content.codepointMask;` * write: `content |= codepoint & Content.codepointMask;` * shortcut if precondition `codepoint <= 0x10FFFF` is met: * `content |= codepoint;` */ CODEPOINT_MASK = 0x1FFFFF, /** * bit 22 flag indication whether a cell contains combined content * read: `isCombined = content & Content.isCombined;` * set: `content |= Content.isCombined;` * clear: `content &= ~Content.isCombined;` */ IS_COMBINED = 0x200000, // 1 << 21 /** * bit 1..22 mask to check whether a cell contains any string data * we need to check for codepoint and isCombined bits to see * whether a cell contains anything * read: `isEmtpy = !(content & Content.hasContent)` */ HAS_CONTENT = 0x2FFFFF, /** * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) * read: `width = (content & Content.widthMask) >> Content.widthShift;` * `hasWidth = content & Content.widthMask;` * as long as wcwidth is highest value in `content`: * `width = content >> Content.widthShift;` * write: `content |= (width << Content.widthShift) & Content.widthMask;` * shortcut if precondition `0 <= width <= 3` is met: * `content |= width << Content.widthShift;` */ WIDTH_MASK = 0xC00000, // 3 << 22 WIDTH_SHIFT = 22 } /** * Typed array based bufferline implementation. */ export class BufferLine implements IBufferLine { protected _data: Uint32Array | null = null; protected _combined: {[index: number]: string} = {}; public length: number; constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) { if (!fillCharData) { fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; } if (cols) { this._data = new Uint32Array(cols * CELL_SIZE); for (let i = 0; i < cols; ++i) { this.set(i, fillCharData); } } this.length = cols; } public get(index: number): CharData { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; const cp = content & Content.CODEPOINT_MASK; return [ this._data[index * CELL_SIZE + Cell.FG], (content & Content.IS_COMBINED) ? this._combined[index] : (cp) ? String.fromCharCode(cp) : '', content >> Content.WIDTH_SHIFT, (content & Content.IS_COMBINED) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : cp ]; } public set(index: number, value: CharData): void { this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } else { this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } /** * Set cell data from input handler. * Since the input handler see the incoming chars as UTF32 codepoints, * it gets an optimized access method. */ public setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; } /** * Add a char to a cell from input handler. * During input stage combining chars with a width of 0 follow and stack * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ public addCharToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { // we already have a combined string, simply add this._combined[index] += stringFromCodePoint(codePoint); } else { if (content & Content.CODEPOINT_MASK) { // normal case for combining chars: // - move current leading char + new one into combined string // - set codepoint in cell buffer to index // - set combined flag this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); content &= ~Content.CODEPOINT_MASK; content |= index | Content.IS_COMBINED; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 content = codePoint | (1 << Content.WIDTH_SHIFT); } this._data[index * CELL_SIZE + Cell.CONTENT] = content; } } /** * Set data from another buffer cell. * Useful for basic in buffer copy action. */ public setDataFromCellData(index: number, content: number, fg: number, bg: number, combined?: string): void { this._data[index * CELL_SIZE + Cell.CONTENT] = content; this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; if (content & Content.IS_COMBINED && combined) { this._combined[index] = combined; } } public insertCells(pos: number, n: number, fillCharData: CharData): void { pos %= this.length; if (n < this.length - pos) { for (let i = this.length - pos - n - 1; i >= 0; --i) { this.set(pos + n + i, this.get(pos + i)); } for (let i = 0; i < n; ++i) { this.set(pos + i, fillCharData); } } else { for (let i = pos; i < this.length; ++i) { this.set(i, fillCharData); } } } public deleteCells(pos: number, n: number, fillCharData: CharData): void { pos %= this.length; if (n < this.length - pos) { for (let i = 0; i < this.length - pos - n; ++i) { this.set(pos + i, this.get(pos + n + i)); } for (let i = this.length - n; i < this.length; ++i) { this.set(i, fillCharData); } } else { for (let i = pos; i < this.length; ++i) { this.set(i, fillCharData); } } } public replaceCells(start: number, end: number, fillCharData: CharData): void { while (start < end && start < this.length) { this.set(start++, fillCharData); } } public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { if (cols === this.length || (!shrink && cols < this.length)) { return; } if (cols > this.length) { const data = new Uint32Array(cols * CELL_SIZE); if (this.length) { if (cols * CELL_SIZE < this._data.length) { data.set(this._data.subarray(0, cols * CELL_SIZE)); } else { data.set(this._data); } } this._data = data; for (let i = this.length; i < cols; ++i) { this.set(i, fillCharData); } } else if (shrink) { if (cols) { const data = new Uint32Array(cols * CELL_SIZE); data.set(this._data.subarray(0, cols * CELL_SIZE)); this._data = data; } else { this._data = null; } } this.length = cols; } /** fill a line with fillCharData */ public fill(fillCharData: CharData): void { this._combined = {}; for (let i = 0; i < this.length; ++i) { this.set(i, fillCharData); } } /** alter to a full copy of line */ public copyFrom(line: BufferLine): void { if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { // use high speed copy if lengths are equal this._data.set(line._data); } this.length = line.length; this._combined = {}; for (const el in line._combined) { this._combined[el] = line._combined[el]; } this.isWrapped = line.isWrapped; } /** create a new clone */ public clone(): IBufferLine { const newLine = new BufferLine(0); newLine._data = new Uint32Array(this._data); newLine.length = this.length; for (const el in this._combined) { newLine._combined[el] = this._combined[el]; } newLine.isWrapped = this.isWrapped; return newLine; } public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT)) { return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); } } return 0; } public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { endCol = Math.min(endCol, this.getTrimmedLength()); } let result = ''; while (startCol < endCol) { const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; const cp = content & Content.CODEPOINT_MASK; result += (content & Content.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 } return result; } }