2 * Copyright (c) 2018 The xterm.js authors. All rights reserved.
6 import { CharData, IBufferLine, ICellData } from 'common/Types';
7 import { stringFromCodePoint } from 'common/input/TextDecoder';
8 import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
9 import { CellData } from 'common/buffer/CellData';
10 import { AttributeData } from 'common/buffer/AttributeData';
13 * buffer memory layout:
15 * | uint32_t | uint32_t | uint32_t |
16 * | `content` | `FG` | `BG` |
17 * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
21 /** typed array slots taken by one cell */
25 * Cell member indices.
28 * `content = data[column * CELL_SIZE + Cell.CONTENT];`
29 * `fg = data[column * CELL_SIZE + Cell.FG];`
30 * `bg = data[column * CELL_SIZE + Cell.BG];`
34 FG = 1, // currently simply holds all known attrs
35 BG = 2 // currently unused
38 export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
41 * Typed array based bufferline implementation.
43 * There are 2 ways to insert data into the cell buffer:
44 * - `setCellFromCodepoint` + `addCodepointToCell`
45 * Use these for data that is already UTF32.
46 * Used during normal input in `InputHandler` for faster buffer access.
48 * This method takes a CellData object and stores the data in the buffer.
49 * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).
51 * To retrieve data from the buffer use either one of the primitive methods
52 * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
53 * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
55 export class BufferLine implements IBufferLine {
56 protected _data: Uint32Array;
57 protected _combined: {[index: number]: string} = {};
58 public length: number;
60 constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
61 this._data = new Uint32Array(cols * CELL_SIZE);
62 const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
63 for (let i = 0; i < cols; ++i) {
64 this.setCell(i, cell);
70 * Get cell data CharData.
73 public get(index: number): CharData {
74 const content = this._data[index * CELL_SIZE + Cell.CONTENT];
75 const cp = content & Content.CODEPOINT_MASK;
77 this._data[index * CELL_SIZE + Cell.FG],
78 (content & Content.IS_COMBINED_MASK)
79 ? this._combined[index]
80 : (cp) ? stringFromCodePoint(cp) : '',
81 content >> Content.WIDTH_SHIFT,
82 (content & Content.IS_COMBINED_MASK)
83 ? this._combined[index].charCodeAt(this._combined[index].length - 1)
89 * Set cell data from CharData.
92 public set(index: number, value: CharData): void {
93 this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
94 if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
95 this._combined[index] = value[1];
96 this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
98 this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
104 * use these when only one value is needed, otherwise use `loadCell`
106 public getWidth(index: number): number {
107 return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
110 /** Test whether content has width. */
111 public hasWidth(index: number): number {
112 return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
115 /** Get FG cell component. */
116 public getFg(index: number): number {
117 return this._data[index * CELL_SIZE + Cell.FG];
120 /** Get BG cell component. */
121 public getBg(index: number): number {
122 return this._data[index * CELL_SIZE + Cell.BG];
126 * Test whether contains any chars.
127 * Basically an empty has no content, but other cells might differ in FG/BG
128 * from real empty cells.
130 public hasContent(index: number): number {
131 return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
135 * Get codepoint of the cell.
136 * To be in line with `code` in CharData this either returns
137 * a single UTF32 codepoint or the last codepoint of a combined string.
139 public getCodePoint(index: number): number {
140 const content = this._data[index * CELL_SIZE + Cell.CONTENT];
141 if (content & Content.IS_COMBINED_MASK) {
142 return this._combined[index].charCodeAt(this._combined[index].length - 1);
144 return content & Content.CODEPOINT_MASK;
147 /** Test whether the cell contains a combined string. */
148 public isCombined(index: number): number {
149 return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK;
152 /** Returns the string content of the cell. */
153 public getString(index: number): string {
154 const content = this._data[index * CELL_SIZE + Cell.CONTENT];
155 if (content & Content.IS_COMBINED_MASK) {
156 return this._combined[index];
158 if (content & Content.CODEPOINT_MASK) {
159 return stringFromCodePoint(content & Content.CODEPOINT_MASK);
161 // return empty string for empty cells
166 * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly
167 * to GC as it significantly reduced the amount of new objects/references needed.
169 public loadCell(index: number, cell: ICellData): ICellData {
170 const startIndex = index * CELL_SIZE;
171 cell.content = this._data[startIndex + Cell.CONTENT];
172 cell.fg = this._data[startIndex + Cell.FG];
173 cell.bg = this._data[startIndex + Cell.BG];
174 if (cell.content & Content.IS_COMBINED_MASK) {
175 cell.combinedData = this._combined[index];
181 * Set data at `index` to `cell`.
183 public setCell(index: number, cell: ICellData): void {
184 if (cell.content & Content.IS_COMBINED_MASK) {
185 this._combined[index] = cell.combinedData;
187 this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
188 this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
189 this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
193 * Set cell data from input handler.
194 * Since the input handler see the incoming chars as UTF32 codepoints,
195 * it gets an optimized access method.
197 public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void {
198 this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
199 this._data[index * CELL_SIZE + Cell.FG] = fg;
200 this._data[index * CELL_SIZE + Cell.BG] = bg;
204 * Add a codepoint to a cell from input handler.
205 * During input stage combining chars with a width of 0 follow and stack
206 * onto a leading char. Since we already set the attrs
207 * by the previous `setDataFromCodePoint` call, we can omit it here.
209 public addCodepointToCell(index: number, codePoint: number): void {
210 let content = this._data[index * CELL_SIZE + Cell.CONTENT];
211 if (content & Content.IS_COMBINED_MASK) {
212 // we already have a combined string, simply add
213 this._combined[index] += stringFromCodePoint(codePoint);
215 if (content & Content.CODEPOINT_MASK) {
216 // normal case for combining chars:
217 // - move current leading char + new one into combined string
218 // - set combined flag
219 this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
220 content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
221 content |= Content.IS_COMBINED_MASK;
223 // should not happen - we actually have no data in the cell yet
224 // simply set the data in the cell buffer with a width of 1
225 content = codePoint | (1 << Content.WIDTH_SHIFT);
227 this._data[index * CELL_SIZE + Cell.CONTENT] = content;
231 public insertCells(pos: number, n: number, fillCellData: ICellData): void {
233 if (n < this.length - pos) {
234 const cell = new CellData();
235 for (let i = this.length - pos - n - 1; i >= 0; --i) {
236 this.setCell(pos + n + i, this.loadCell(pos + i, cell));
238 for (let i = 0; i < n; ++i) {
239 this.setCell(pos + i, fillCellData);
242 for (let i = pos; i < this.length; ++i) {
243 this.setCell(i, fillCellData);
248 public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
250 if (n < this.length - pos) {
251 const cell = new CellData();
252 for (let i = 0; i < this.length - pos - n; ++i) {
253 this.setCell(pos + i, this.loadCell(pos + n + i, cell));
255 for (let i = this.length - n; i < this.length; ++i) {
256 this.setCell(i, fillCellData);
259 for (let i = pos; i < this.length; ++i) {
260 this.setCell(i, fillCellData);
265 public replaceCells(start: number, end: number, fillCellData: ICellData): void {
266 while (start < end && start < this.length) {
267 this.setCell(start++, fillCellData);
271 public resize(cols: number, fillCellData: ICellData): void {
272 if (cols === this.length) {
275 if (cols > this.length) {
276 const data = new Uint32Array(cols * CELL_SIZE);
278 if (cols * CELL_SIZE < this._data.length) {
279 data.set(this._data.subarray(0, cols * CELL_SIZE));
281 data.set(this._data);
285 for (let i = this.length; i < cols; ++i) {
286 this.setCell(i, fillCellData);
290 const data = new Uint32Array(cols * CELL_SIZE);
291 data.set(this._data.subarray(0, cols * CELL_SIZE));
293 // Remove any cut off combined data
294 const keys = Object.keys(this._combined);
295 for (let i = 0; i < keys.length; i++) {
296 const key = parseInt(keys[i], 10);
298 delete this._combined[key];
302 this._data = new Uint32Array(0);
309 /** fill a line with fillCharData */
310 public fill(fillCellData: ICellData): void {
312 for (let i = 0; i < this.length; ++i) {
313 this.setCell(i, fillCellData);
317 /** alter to a full copy of line */
318 public copyFrom(line: BufferLine): void {
319 if (this.length !== line.length) {
320 this._data = new Uint32Array(line._data);
322 // use high speed copy if lengths are equal
323 this._data.set(line._data);
325 this.length = line.length;
327 for (const el in line._combined) {
328 this._combined[el] = line._combined[el];
330 this.isWrapped = line.isWrapped;
333 /** create a new clone */
334 public clone(): IBufferLine {
335 const newLine = new BufferLine(0);
336 newLine._data = new Uint32Array(this._data);
337 newLine.length = this.length;
338 for (const el in this._combined) {
339 newLine._combined[el] = this._combined[el];
341 newLine.isWrapped = this.isWrapped;
345 public getTrimmedLength(): number {
346 for (let i = this.length - 1; i >= 0; --i) {
347 if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
348 return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
354 public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
355 const srcData = src._data;
356 if (applyInReverse) {
357 for (let cell = length - 1; cell >= 0; cell--) {
358 for (let i = 0; i < CELL_SIZE; i++) {
359 this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
363 for (let cell = 0; cell < length; cell++) {
364 for (let i = 0; i < CELL_SIZE; i++) {
365 this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
370 // Move any combined data over as needed
371 const srcCombinedKeys = Object.keys(src._combined);
372 for (let i = 0; i < srcCombinedKeys.length; i++) {
373 const key = parseInt(srcCombinedKeys[i], 10);
375 this._combined[key - srcCol + destCol] = src._combined[key];
380 public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
382 endCol = Math.min(endCol, this.getTrimmedLength());
385 while (startCol < endCol) {
386 const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
387 const cp = content & Content.CODEPOINT_MASK;
388 result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
389 startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1