2 * Copyright (c) 2019 The xterm.js authors. All rights reserved.
6 import { BufferLine } from 'common/buffer/BufferLine';
7 import { CircularList } from 'common/CircularList';
8 import { IBufferLine, ICellData } from 'common/Types';
10 export interface INewLayoutResult {
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.
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[] = [];
26 for (let y = 0; y < lines.length - 1; y++) {
27 // Check if this row is wrapped
29 let nextLine = lines.get(++i) as BufferLine;
30 if (!nextLine.isWrapped) {
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;
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;
48 // Copy buffer data to new locations
49 let destLineIndex = 0;
50 let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);
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);
59 wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);
61 destCol += cellsToCopy;
62 if (destCol === newCols) {
66 srcCol += cellsToCopy;
67 if (srcCol === srcTrimmedTineLength) {
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);
82 // Clear out remaining cells or fragments could remain;
83 wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);
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) {
95 if (countToRemove > 0) {
96 toRemove.push(y + wrappedLines.length - countToRemove); // index
97 toRemove.push(countToRemove);
100 y += wrappedLines.length - 1;
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.
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];
120 // Tell markers that there was a deletion
121 lines.onDeleteEmitter.fire({
122 index: i - countRemovedSoFar,
123 amount: countToRemove
126 i += countToRemove - 1;
127 countRemovedSoFar += countToRemove;
128 nextToRemoveStart = toRemove[++nextToRemoveIndex];
135 countRemoved: countRemovedSoFar
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.
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);
152 // Rearrange the list
153 for (let i = 0; i < newLayoutLines.length; i++) {
154 lines.set(i, newLayoutLines[i]);
156 lines.length = newLayout.length;
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.
167 * Calling this with a `newCols` value of `1` will lock up.
169 * @param wrappedLines The wrapped lines to evaluate.
170 * @param oldCols The columns before resize.
171 * @param newCols The columns after resize.
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);
177 // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
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);
189 const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);
190 if (srcCol > oldTrimmedLength) {
191 srcCol -= oldTrimmedLength;
194 const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;
198 const lineLength = endsWithWide ? newCols - 1 : newCols;
199 newLineLengths.push(lineLength);
200 cellsAvailable += lineLength;
203 return newLineLengths;
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();
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
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) {