xterm
[VSoRC/.git] / node_modules / xterm / src / common / buffer / BufferReflow.ts
1 /**
2  * Copyright (c) 2019 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
5
6 import { BufferLine } from 'common/buffer/BufferLine';
7 import { CircularList } from 'common/CircularList';
8 import { IBufferLine, ICellData } from 'common/Types';
9
10 export interface INewLayoutResult {
11   layout: number[];
12   countRemoved: number;
13 }
14
15 /**
16  * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
17  * when a wrapped line unwraps.
18  * @param lines The buffer lines.
19  * @param newCols The columns after resize.
20  */
21 export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
22   // Gather all BufferLines that need to be removed from the Buffer here so that they can be
23   // batched up and only committed once
24   const toRemove: number[] = [];
25
26   for (let y = 0; y < lines.length - 1; y++) {
27     // Check if this row is wrapped
28     let i = y;
29     let nextLine = lines.get(++i) as BufferLine;
30     if (!nextLine.isWrapped) {
31       continue;
32     }
33
34     // Check how many lines it's wrapped for
35     const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];
36     while (i < lines.length && nextLine.isWrapped) {
37       wrappedLines.push(nextLine);
38       nextLine = lines.get(++i) as BufferLine;
39     }
40
41     // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
42     // lines with the cursor
43     if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
44       y += wrappedLines.length - 1;
45       continue;
46     }
47
48     // Copy buffer data to new locations
49     let destLineIndex = 0;
50     let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);
51     let srcLineIndex = 1;
52     let srcCol = 0;
53     while (srcLineIndex < wrappedLines.length) {
54       const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);
55       const srcRemainingCells = srcTrimmedTineLength - srcCol;
56       const destRemainingCells = newCols - destCol;
57       const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);
58
59       wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);
60
61       destCol += cellsToCopy;
62       if (destCol === newCols) {
63         destLineIndex++;
64         destCol = 0;
65       }
66       srcCol += cellsToCopy;
67       if (srcCol === srcTrimmedTineLength) {
68         srcLineIndex++;
69         srcCol = 0;
70       }
71
72       // Make sure the last cell isn't wide, if it is copy it to the current dest
73       if (destCol === 0 && destLineIndex !== 0) {
74         if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {
75           wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);
76           // Null out the end of the last row
77           wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);
78         }
79       }
80     }
81
82     // Clear out remaining cells or fragments could remain;
83     wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);
84
85     // Work backwards and remove any rows at the end that only contain null cells
86     let countToRemove = 0;
87     for (let i = wrappedLines.length - 1; i > 0; i--) {
88       if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {
89         countToRemove++;
90       } else {
91         break;
92       }
93     }
94
95     if (countToRemove > 0) {
96       toRemove.push(y + wrappedLines.length - countToRemove); // index
97       toRemove.push(countToRemove);
98     }
99
100     y += wrappedLines.length - 1;
101   }
102   return toRemove;
103 }
104
105 /**
106  * Creates and return the new layout for lines given an array of indexes to be removed.
107  * @param lines The buffer lines.
108  * @param toRemove The indexes to remove.
109  */
110 export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, toRemove: number[]): INewLayoutResult {
111   const layout: number[] = [];
112   // First iterate through the list and get the actual indexes to use for rows
113   let nextToRemoveIndex = 0;
114   let nextToRemoveStart = toRemove[nextToRemoveIndex];
115   let countRemovedSoFar = 0;
116   for (let i = 0; i < lines.length; i++) {
117     if (nextToRemoveStart === i) {
118       const countToRemove = toRemove[++nextToRemoveIndex];
119
120       // Tell markers that there was a deletion
121       lines.onDeleteEmitter.fire({
122         index: i - countRemovedSoFar,
123         amount: countToRemove
124       });
125
126       i += countToRemove - 1;
127       countRemovedSoFar += countToRemove;
128       nextToRemoveStart = toRemove[++nextToRemoveIndex];
129     } else {
130       layout.push(i);
131     }
132   }
133   return {
134     layout,
135     countRemoved: countRemovedSoFar
136   };
137 }
138
139 /**
140  * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's
141  * done all at once in a single iteration through the list since splice is very expensive.
142  * @param lines The buffer lines.
143  * @param newLayout The new layout to apply.
144  */
145 export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, newLayout: number[]): void {
146   // Record original lines so they don't get overridden when we rearrange the list
147   const newLayoutLines: BufferLine[] = [];
148   for (let i = 0; i < newLayout.length; i++) {
149     newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);
150   }
151
152   // Rearrange the list
153   for (let i = 0; i < newLayoutLines.length; i++) {
154     lines.set(i, newLayoutLines[i]);
155   }
156   lines.length = newLayout.length;
157 }
158
159 /**
160  * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
161  * compute the wrapping points since wide characters may need to be wrapped onto the following line.
162  * This function will return an array of numbers of where each line wraps to, the resulting array
163  * will only contain the values `newCols` (when the line does not end with a wide character) and
164  * `newCols - 1` (when the line does end with a wide character), except for the last value which
165  * will contain the remaining items to fill the line.
166  *
167  * Calling this with a `newCols` value of `1` will lock up.
168  *
169  * @param wrappedLines The wrapped lines to evaluate.
170  * @param oldCols The columns before resize.
171  * @param newCols The columns after resize.
172  */
173 export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {
174   const newLineLengths: number[] = [];
175   const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c);
176
177   // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
178   // linesNeeded
179   let srcCol = 0;
180   let srcLine = 0;
181   let cellsAvailable = 0;
182   while (cellsAvailable < cellsNeeded) {
183     if (cellsNeeded - cellsAvailable < newCols) {
184       // Add the final line and exit the loop
185       newLineLengths.push(cellsNeeded - cellsAvailable);
186       break;
187     }
188     srcCol += newCols;
189     const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);
190     if (srcCol > oldTrimmedLength) {
191       srcCol -= oldTrimmedLength;
192       srcLine++;
193     }
194     const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;
195     if (endsWithWide) {
196       srcCol--;
197     }
198     const lineLength = endsWithWide ? newCols - 1 : newCols;
199     newLineLengths.push(lineLength);
200     cellsAvailable += lineLength;
201   }
202
203   return newLineLengths;
204 }
205
206 export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {
207   // If this is the last row in the wrapped line, get the actual trimmed length
208   if (i === lines.length - 1) {
209     return lines[i].getTrimmedLength();
210   }
211   // Detect whether the following line starts with a wide character and the end of the current line
212   // is null, if so then we can be pretty sure the null character should be excluded from the line
213   // length]
214   const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;
215   const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;
216   if (endsInNull && followingLineStartsWithWide) {
217     return cols - 1;
218   }
219   return cols;
220 }