Files
xterm.js/src/BufferLine.ts
T

363 lines
13 KiB
TypeScript

/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CharData, IBufferLine, ICellData } 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`.
*/
export 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
}
export class CellData implements ICellData {
public content: number = 0;
public fg: number = 0;
public bg: number = 0;
public combinedData: string = '';
public get combined(): number {
return this.content & Content.IS_COMBINED;
}
public get width(): number {
return this.content >> Content.WIDTH_SHIFT;
}
public get chars(): string {
return (this.content & Content.IS_COMBINED) ? this.combinedData : stringFromCodePoint(this.content & Content.CODEPOINT_MASK);
}
public get code(): number {
return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK);
}
public setFromCharData(value: CharData): void {
this.fg = value[CHAR_DATA_ATTR_INDEX];
this.bg = 0;
let combined = false;
if (value[CHAR_DATA_CHAR_INDEX].length > 2) {
combined = true;
} else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);
if (0xD800 <= code && code <= 0xDBFF) {
const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);
if (0xDC00 <= second && second <= 0xDFFF) {
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
combined = true;
}
}
combined = true;
} else {
this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
if (combined) {
this.combinedData = value[CHAR_DATA_CHAR_INDEX];
this.content = Content.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
}
}
/**
* Typed array based bufferline implementation.
*/
export class BufferLine implements IBufferLine {
protected _data: Uint32Array | null = null;
protected _combined: {[index: number]: string} = {};
protected _cell: CellData = new CellData();
public length: number;
constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) {
if (cols) {
this._data = new Uint32Array(cols * CELL_SIZE);
this._cell.setFromCharData(fillCharData || [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
for (let i = 0; i < cols; ++i) {
this.setCell(i, this._cell);
}
}
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) ? stringFromCodePoint(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);
}
}
public loadCell(index: number, cell: ICellData): ICellData {
cell.content = this._data[index * CELL_SIZE + Cell.CONTENT];
cell.fg = this._data[index * CELL_SIZE + Cell.FG];
cell.bg = this._data[index * CELL_SIZE + Cell.BG];
if (cell.content & Content.IS_COMBINED) {
cell.combinedData = this._combined[index];
}
return cell;
}
public setCell(index: number, cell: ICellData): void {
if (cell.content & Content.IS_COMBINED) {
this._combined[index] = cell.combinedData;
// we also need to clear and set codepoint to index
cell.content &= ~Content.CODEPOINT_MASK;
cell.content |= index;
}
this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
}
/**
* 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;
}
}
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.setCell(pos + n + i, this.loadCell(pos + i, this._cell));
}
this._cell.setFromCharData(fillCharData);
for (let i = 0; i < n; ++i) {
this.setCell(pos + i, this._cell);
}
} else {
this._cell.setFromCharData(fillCharData);
for (let i = pos; i < this.length; ++i) {
this.setCell(i, this._cell);
}
}
}
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.setCell(pos + i, this.loadCell(pos + n + i, this._cell));
}
this._cell.setFromCharData(fillCharData);
for (let i = this.length - n; i < this.length; ++i) {
this.setCell(i, this._cell);
}
} else {
this._cell.setFromCharData(fillCharData);
for (let i = pos; i < this.length; ++i) {
this.setCell(i, this._cell);
}
}
}
public replaceCells(start: number, end: number, fillCharData: CharData): void {
this._cell.setFromCharData(fillCharData);
while (start < end && start < this.length) {
this.setCell(start++, this._cell);
}
}
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;
this._cell.setFromCharData(fillCharData);
for (let i = this.length; i < cols; ++i) {
this.setCell(i, this._cell);
}
} 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 = {};
this._cell.setFromCharData(fillCharData);
for (let i = 0; i < this.length; ++i) {
this.setCell(i, this._cell);
}
}
/** 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;
}
}