xterm
[VSoRC/.git] / node_modules / xterm / src / common / buffer / BufferLine.ts
1 /**
2  * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
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';
11
12 /**
13  * buffer memory layout:
14  *
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) |
18  */
19
20
21 /** typed array slots taken by one cell */
22 const CELL_SIZE = 3;
23
24 /**
25  * Cell member indices.
26  *
27  * Direct access:
28  *    `content = data[column * CELL_SIZE + Cell.CONTENT];`
29  *    `fg = data[column * CELL_SIZE + Cell.FG];`
30  *    `bg = data[column * CELL_SIZE + Cell.BG];`
31  */
32 const enum Cell {
33   CONTENT = 0,
34   FG = 1, // currently simply holds all known attrs
35   BG = 2  // currently unused
36 }
37
38 export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
39
40 /**
41  * Typed array based bufferline implementation.
42  *
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.
47  * - `setCell`
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).
50  *
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.
54  */
55 export class BufferLine implements IBufferLine {
56   protected _data: Uint32Array;
57   protected _combined: {[index: number]: string} = {};
58   public length: number;
59
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);
65     }
66     this.length = cols;
67   }
68
69   /**
70    * Get cell data CharData.
71    * @deprecated
72    */
73   public get(index: number): CharData {
74     const content = this._data[index * CELL_SIZE + Cell.CONTENT];
75     const cp = content & Content.CODEPOINT_MASK;
76     return [
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)
84         : cp
85     ];
86   }
87
88   /**
89    * Set cell data from CharData.
90    * @deprecated
91    */
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);
97     } else {
98       this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
99     }
100   }
101
102   /**
103    * primitive getters
104    * use these when only one value is needed, otherwise use `loadCell`
105    */
106   public getWidth(index: number): number {
107     return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
108   }
109
110   /** Test whether content has width. */
111   public hasWidth(index: number): number {
112     return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
113   }
114
115   /** Get FG cell component. */
116   public getFg(index: number): number {
117     return this._data[index * CELL_SIZE + Cell.FG];
118   }
119
120   /** Get BG cell component. */
121   public getBg(index: number): number {
122     return this._data[index * CELL_SIZE + Cell.BG];
123   }
124
125   /**
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.
129    * */
130   public hasContent(index: number): number {
131     return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
132   }
133
134   /**
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.
138    */
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);
143     }
144     return content & Content.CODEPOINT_MASK;
145   }
146
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;
150   }
151
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];
157     }
158     if (content & Content.CODEPOINT_MASK) {
159       return stringFromCodePoint(content & Content.CODEPOINT_MASK);
160     }
161     // return empty string for empty cells
162     return '';
163   }
164
165   /**
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.
168    */
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];
176     }
177     return cell;
178   }
179
180   /**
181    * Set data at `index` to `cell`.
182    */
183   public setCell(index: number, cell: ICellData): void {
184     if (cell.content & Content.IS_COMBINED_MASK) {
185       this._combined[index] = cell.combinedData;
186     }
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;
190   }
191
192   /**
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.
196    */
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;
201   }
202
203   /**
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.
208    */
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);
214     } else {
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;
222       } else {
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);
226       }
227       this._data[index * CELL_SIZE + Cell.CONTENT] = content;
228     }
229   }
230
231   public insertCells(pos: number, n: number, fillCellData: ICellData): void {
232     pos %= this.length;
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));
237       }
238       for (let i = 0; i < n; ++i) {
239         this.setCell(pos + i, fillCellData);
240       }
241     } else {
242       for (let i = pos; i < this.length; ++i) {
243         this.setCell(i, fillCellData);
244       }
245     }
246   }
247
248   public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
249     pos %= this.length;
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));
254       }
255       for (let i = this.length - n; i < this.length; ++i) {
256         this.setCell(i, fillCellData);
257       }
258     } else {
259       for (let i = pos; i < this.length; ++i) {
260         this.setCell(i, fillCellData);
261       }
262     }
263   }
264
265   public replaceCells(start: number, end: number, fillCellData: ICellData): void {
266     while (start < end  && start < this.length) {
267       this.setCell(start++, fillCellData);
268     }
269   }
270
271   public resize(cols: number, fillCellData: ICellData): void {
272     if (cols === this.length) {
273       return;
274     }
275     if (cols > this.length) {
276       const data = new Uint32Array(cols * CELL_SIZE);
277       if (this.length) {
278         if (cols * CELL_SIZE < this._data.length) {
279           data.set(this._data.subarray(0, cols * CELL_SIZE));
280         } else {
281           data.set(this._data);
282         }
283       }
284       this._data = data;
285       for (let i = this.length; i < cols; ++i) {
286         this.setCell(i, fillCellData);
287       }
288     } else {
289       if (cols) {
290         const data = new Uint32Array(cols * CELL_SIZE);
291         data.set(this._data.subarray(0, cols * CELL_SIZE));
292         this._data = data;
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);
297           if (key >= cols) {
298             delete this._combined[key];
299           }
300         }
301       } else {
302         this._data = new Uint32Array(0);
303         this._combined = {};
304       }
305     }
306     this.length = cols;
307   }
308
309   /** fill a line with fillCharData */
310   public fill(fillCellData: ICellData): void {
311     this._combined = {};
312     for (let i = 0; i < this.length; ++i) {
313       this.setCell(i, fillCellData);
314     }
315   }
316
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);
321     } else {
322       // use high speed copy if lengths are equal
323       this._data.set(line._data);
324     }
325     this.length = line.length;
326     this._combined = {};
327     for (const el in line._combined) {
328       this._combined[el] = line._combined[el];
329     }
330     this.isWrapped = line.isWrapped;
331   }
332
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];
340     }
341     newLine.isWrapped = this.isWrapped;
342     return newLine;
343   }
344
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);
349       }
350     }
351     return 0;
352   }
353
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];
360         }
361       }
362     } else {
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];
366         }
367       }
368     }
369
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);
374       if (key >= srcCol) {
375         this._combined[key - srcCol + destCol] = src._combined[key];
376       }
377     }
378   }
379
380   public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
381     if (trimRight) {
382       endCol = Math.min(endCol, this.getTrimmedLength());
383     }
384     let result = '';
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
390     }
391     return result;
392   }
393 }