buffer_reflow.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. import 'dart:math';
  2. import 'package:xterm/buffer/cell_attr.dart';
  3. import 'buffer.dart';
  4. import 'buffer_line.dart';
  5. class LayoutResult {
  6. LayoutResult(this.layout, this.removedCount);
  7. final List<int> layout;
  8. final int removedCount;
  9. }
  10. class InsertionSet {
  11. InsertionSet({this.lines, this.start, this.isNull = false});
  12. final List<BufferLine>? lines;
  13. final int? start;
  14. final bool isNull;
  15. static InsertionSet nullValue = InsertionSet(isNull: true);
  16. }
  17. class BufferReflow {
  18. BufferReflow(this._buffer, this._emptyCellAttr);
  19. final Buffer _buffer;
  20. final CellAttr _emptyCellAttr;
  21. void doReflow(int colsBefore, int colsAfter) {
  22. if (colsBefore == colsAfter) {
  23. return;
  24. }
  25. if (colsAfter > colsBefore) {
  26. //got larger
  27. _reflowLarger(colsBefore, colsAfter);
  28. } else {
  29. //got smaller
  30. _reflowSmaller(colsBefore, colsAfter);
  31. }
  32. }
  33. void _reflowLarger(int colsBefore, int colsAfter) {
  34. var toRemove = _reflowLargerGetLinesToRemove(colsBefore, colsAfter);
  35. if (toRemove.length > 0) {
  36. var newLayoutResult =
  37. _reflowLargerCreateNewLayout(_buffer.lines, toRemove);
  38. _reflowLargerApplyNewLayout(_buffer.lines, newLayoutResult.layout);
  39. _reflowLargerAdjustViewport(
  40. colsBefore, colsAfter, newLayoutResult.removedCount);
  41. }
  42. }
  43. void _reflowSmaller(int colsBefore, int colsAfter) {
  44. // Gather all BufferLines that need to be inserted into the Buffer here so that they can be
  45. // batched up and only committed once
  46. List<InsertionSet> toInsert = [];
  47. int countToInsert = 0;
  48. // Go backwards as many lines may be trimmed and this will avoid considering them
  49. for (int y = _buffer.lines.length - 1; y >= 0; y--) {
  50. // Check whether this line is a problem or not, if not skip it
  51. BufferLine nextLine = _buffer.lines[y];
  52. int lineLength = nextLine.getTrimmedLength();
  53. if (!nextLine.isWrapped && lineLength <= colsAfter) {
  54. continue;
  55. }
  56. // Gather wrapped lines and adjust y to be the starting line
  57. List<BufferLine> wrappedLines = [];
  58. wrappedLines.add(nextLine);
  59. while (nextLine.isWrapped && y > 0) {
  60. nextLine = _buffer.lines[--y];
  61. wrappedLines.insert(0, nextLine);
  62. }
  63. // If these lines contain the cursor don't touch them, the program will handle fixing up
  64. // wrapped lines with the cursor
  65. final absoluteY = _buffer.cursorY - _buffer.scrollOffsetFromBottom;
  66. if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
  67. continue;
  68. }
  69. int lastLineLength = wrappedLines.last.getTrimmedLength();
  70. List<int> destLineLengths =
  71. _getNewLineLengths(wrappedLines, colsBefore, colsAfter);
  72. int linesToAdd = destLineLengths.length - wrappedLines.length;
  73. // Add the new lines
  74. List<BufferLine> newLines = [];
  75. for (int i = 0; i < linesToAdd; i++) {
  76. BufferLine newLine = BufferLine(numOfCells: colsAfter);
  77. newLines.add(newLine);
  78. }
  79. if (newLines.length > 0) {
  80. toInsert.add(InsertionSet(
  81. start: y + wrappedLines.length + countToInsert, lines: newLines));
  82. countToInsert += newLines.length;
  83. }
  84. newLines.forEach((l) => wrappedLines.add(l));
  85. // Copy buffer data to new locations, this needs to happen backwards to do in-place
  86. int destLineIndex =
  87. destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);
  88. int destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;
  89. if (destCol == 0) {
  90. destLineIndex--;
  91. destCol = destLineLengths[destLineIndex];
  92. }
  93. int srcLineIndex = wrappedLines.length - linesToAdd - 1;
  94. int srcCol = lastLineLength;
  95. while (srcLineIndex >= 0) {
  96. int cellsToCopy = min(srcCol, destCol);
  97. wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex],
  98. srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy);
  99. destCol -= cellsToCopy;
  100. if (destCol == 0) {
  101. destLineIndex--;
  102. if (destLineIndex >= 0) destCol = destLineLengths[destLineIndex];
  103. }
  104. srcCol -= cellsToCopy;
  105. if (srcCol == 0) {
  106. srcLineIndex--;
  107. int wrappedLinesIndex = max(srcLineIndex, 0);
  108. srcCol = _getWrappedLineTrimmedLengthRow(
  109. wrappedLines, wrappedLinesIndex, colsBefore);
  110. }
  111. }
  112. // Null out the end of the line ends if a wide character wrapped to the following line
  113. for (int i = 0; i < wrappedLines.length; i++) {
  114. if (destLineLengths[i] < colsAfter) {
  115. wrappedLines[i].removeRange(destLineLengths[i]);
  116. }
  117. }
  118. // Adjust viewport as needed
  119. //TODO: probably nothing to do here because of the way the ViewPort is handled compared to the xterm.js project
  120. // int viewportAdjustments = linesToAdd;
  121. // while (viewportAdjustments-- > 0) {
  122. // if (Buffer.YBase == 0) {
  123. // if (Buffer.Y < newRows - 1) {
  124. // Buffer.Y++;
  125. // Buffer.Lines.Pop();
  126. // } else {
  127. // Buffer.YBase++;
  128. // Buffer.YDisp++;
  129. // }
  130. // } else {
  131. // // Ensure ybase does not exceed its maximum value
  132. // if (Buffer.YBase <
  133. // Math.Min(Buffer.Lines.MaxLength,
  134. // Buffer.Lines.Length + countToInsert) -
  135. // newRows) {
  136. // if (Buffer.YBase == Buffer.YDisp) {
  137. // Buffer.YDisp++;
  138. // }
  139. //
  140. // Buffer.YBase++;
  141. // }
  142. // }
  143. // }
  144. _buffer.adjustSavedCursor(0, linesToAdd);
  145. //TODO: maybe row count has to be handled here?
  146. }
  147. _rearrange(toInsert, countToInsert);
  148. }
  149. void _rearrange(List<InsertionSet> toInsert, int countToInsert) {
  150. // Rearrange lines in the buffer if there are any insertions, this is done at the end rather
  151. // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
  152. // costly calls to CircularList.splice.
  153. if (toInsert.length > 0) {
  154. // Record buffer insert events and then play them back backwards so that the indexes are
  155. // correct
  156. List<int> insertEvents = [];
  157. // Record original lines so they don't get overridden when we rearrange the list
  158. List<BufferLine> originalLines = List<BufferLine>.from(_buffer.lines);
  159. _buffer.lines.addAll(List<BufferLine>.generate(countToInsert,
  160. (index) => BufferLine(numOfCells: _buffer.terminal.viewWidth)));
  161. int originalLinesLength = originalLines.length;
  162. int originalLineIndex = originalLinesLength - 1;
  163. int nextToInsertIndex = 0;
  164. InsertionSet nextToInsert = toInsert[nextToInsertIndex];
  165. //TODO: remove rows that now are "too much"
  166. int countInsertedSoFar = 0;
  167. for (int i = originalLinesLength + countToInsert - 1; i >= 0; i--) {
  168. if (!nextToInsert.isNull &&
  169. nextToInsert.start != null &&
  170. nextToInsert.lines != null &&
  171. nextToInsert.start! > originalLineIndex + countInsertedSoFar) {
  172. // Insert extra lines here, adjusting i as needed
  173. for (int nextI = nextToInsert.lines!.length - 1;
  174. nextI >= 0;
  175. nextI--) {
  176. if (i < 0) {
  177. // if we reflow and the content has to be scrolled back past the beginning
  178. // of the buffer then we end up loosing those lines
  179. break;
  180. }
  181. _buffer.lines[i--] = nextToInsert.lines![nextI];
  182. }
  183. i++;
  184. countInsertedSoFar += nextToInsert.lines!.length;
  185. if (nextToInsertIndex < toInsert.length - 1) {
  186. nextToInsert = toInsert[++nextToInsertIndex];
  187. } else {
  188. nextToInsert = InsertionSet.nullValue;
  189. }
  190. } else {
  191. _buffer.lines[i] = originalLines[originalLineIndex--];
  192. }
  193. }
  194. }
  195. }
  196. /// <summary>
  197. /// Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
  198. /// compute the wrapping points since wide characters may need to be wrapped onto the following line.
  199. /// This function will return an array of numbers of where each line wraps to, the resulting array
  200. /// will only contain the values `newCols` (when the line does not end with a wide character) and
  201. /// `newCols - 1` (when the line does end with a wide character), except for the last value which
  202. /// will contain the remaining items to fill the line.
  203. /// Calling this with a `newCols` value of `1` will lock up.
  204. /// </summary>
  205. List<int> _getNewLineLengths(
  206. List<BufferLine> wrappedLines, int oldCols, int newCols) {
  207. List<int> newLineLengths = [];
  208. int cellsNeeded = 0;
  209. for (int i = 0; i < wrappedLines.length; i++) {
  210. cellsNeeded += _getWrappedLineTrimmedLengthRow(wrappedLines, i, oldCols);
  211. }
  212. // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
  213. // linesNeeded
  214. int srcCol = 0;
  215. int srcLine = 0;
  216. int cellsAvailable = 0;
  217. while (cellsAvailable < cellsNeeded) {
  218. if (cellsNeeded - cellsAvailable < newCols) {
  219. // Add the final line and exit the loop
  220. newLineLengths.add(cellsNeeded - cellsAvailable);
  221. break;
  222. }
  223. srcCol += newCols;
  224. int oldTrimmedLength =
  225. _getWrappedLineTrimmedLengthRow(wrappedLines, srcLine, oldCols);
  226. if (srcCol > oldTrimmedLength) {
  227. srcCol -= oldTrimmedLength;
  228. srcLine++;
  229. }
  230. bool endsWithWide = wrappedLines[srcLine].getWidthAt(srcCol - 1) == 2;
  231. if (endsWithWide) {
  232. srcCol--;
  233. }
  234. int lineLength = endsWithWide ? newCols - 1 : newCols;
  235. newLineLengths.add(lineLength);
  236. cellsAvailable += lineLength;
  237. }
  238. return newLineLengths;
  239. }
  240. void _reflowLargerAdjustViewport(
  241. int colsBefore, int colsAfter, int countRemoved) {
  242. // Adjust viewport based on number of items removed
  243. var viewportAdjustments = countRemoved;
  244. while (viewportAdjustments-- > 0) {
  245. //viewport is at the top
  246. if (_buffer.lines.length <= _buffer.terminal.viewHeight) {
  247. //cursor is not at the top
  248. if (_buffer.cursorY > 0) {
  249. _buffer.moveCursorY(-1);
  250. }
  251. //buffer doesn't have enough lines
  252. if (_buffer.lines.length < _buffer.terminal.viewHeight) {
  253. // Add an extra row at the bottom of the viewport
  254. _buffer.lines
  255. .add(new BufferLine(numOfCells: colsAfter, attr: _emptyCellAttr));
  256. }
  257. } else {
  258. //Nothing to do here due to the way scrolling is handled
  259. // //user didn't scroll
  260. // if (this.ydisp === this.ybase) {
  261. // //scroll viewport according to...
  262. // this.ydisp--;
  263. // }
  264. // //base window
  265. // this.ybase--;
  266. }
  267. }
  268. //TODO: adjust buffer content to max length
  269. _buffer.adjustSavedCursor(0, -countRemoved);
  270. }
  271. void _reflowLargerApplyNewLayout(
  272. List<BufferLine> lines, List<int> newLayout) {
  273. var newLayoutLines = List<BufferLine>.empty();
  274. for (int i = 0; i < newLayout.length; i++) {
  275. newLayoutLines.add(lines[newLayout[i]]);
  276. }
  277. // Rearrange the list
  278. for (int i = 0; i < newLayoutLines.length; i++) {
  279. lines[i] = newLayoutLines[i];
  280. }
  281. lines.removeRange(newLayoutLines.length, lines.length - 1);
  282. }
  283. LayoutResult _reflowLargerCreateNewLayout(
  284. List<BufferLine> lines, List<int> toRemove) {
  285. var layout = List<int>.empty();
  286. // First iterate through the list and get the actual indexes to use for rows
  287. int nextToRemoveIndex = 0;
  288. int nextToRemoveStart = toRemove[nextToRemoveIndex];
  289. int countRemovedSoFar = 0;
  290. for (int i = 0; i < lines.length; i++) {
  291. if (nextToRemoveStart == i) {
  292. int countToRemove = toRemove[++nextToRemoveIndex];
  293. // Tell markers that there was a deletion
  294. //lines.onDeleteEmitter.fire ({
  295. // index: i - countRemovedSoFar,
  296. // amount: countToRemove
  297. //});
  298. i += countToRemove - 1;
  299. countRemovedSoFar += countToRemove;
  300. nextToRemoveStart = lines.length + 1;
  301. if (nextToRemoveIndex < toRemove.length - 1)
  302. nextToRemoveStart = toRemove[++nextToRemoveIndex];
  303. } else {
  304. layout.add(i);
  305. }
  306. }
  307. return LayoutResult(layout, countRemovedSoFar);
  308. }
  309. List<int> _reflowLargerGetLinesToRemove(int colsBefore, int colsAfter) {
  310. List<int> toRemove = List<int>.empty();
  311. for (int y = 0; y < _buffer.lines.length - 1; y++) {
  312. // Check if this row is wrapped
  313. int i = y;
  314. BufferLine nextLine = _buffer.lines[++i];
  315. if (!nextLine.isWrapped) {
  316. continue;
  317. }
  318. // Check how many lines it's wrapped for
  319. List<BufferLine> wrappedLines = List<BufferLine>.empty();
  320. wrappedLines.add(_buffer.lines[y]);
  321. while (i < _buffer.lines.length && nextLine.isWrapped) {
  322. wrappedLines.add(nextLine);
  323. nextLine = _buffer.lines[++i];
  324. }
  325. final bufferAbsoluteY = _buffer.cursorY - _buffer.scrollOffsetFromBottom;
  326. // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
  327. // lines with the cursor
  328. if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
  329. y += wrappedLines.length - 1;
  330. continue;
  331. }
  332. // Copy buffer data to new locations
  333. int destLineIndex = 0;
  334. int destCol = _getWrappedLineTrimmedLengthRow(
  335. _buffer.lines, destLineIndex, colsBefore);
  336. int srcLineIndex = 1;
  337. int srcCol = 0;
  338. while (srcLineIndex < wrappedLines.length) {
  339. int srcTrimmedTineLength = _getWrappedLineTrimmedLengthRow(
  340. wrappedLines, srcLineIndex, colsBefore);
  341. int srcRemainingCells = srcTrimmedTineLength - srcCol;
  342. int destRemainingCells = colsAfter - destCol;
  343. int cellsToCopy = min(srcRemainingCells, destRemainingCells);
  344. wrappedLines[destLineIndex].copyCellsFrom(
  345. wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy);
  346. destCol += cellsToCopy;
  347. if (destCol == colsAfter) {
  348. destLineIndex++;
  349. destCol = 0;
  350. }
  351. srcCol += cellsToCopy;
  352. if (srcCol == srcTrimmedTineLength) {
  353. srcLineIndex++;
  354. srcCol = 0;
  355. }
  356. // Make sure the last cell isn't wide, if it is copy it to the current dest
  357. if (destCol == 0 && destLineIndex != 0) {
  358. if (wrappedLines[destLineIndex - 1].getWidthAt(colsAfter - 1) == 2) {
  359. wrappedLines[destLineIndex].copyCellsFrom(
  360. wrappedLines[destLineIndex - 1], colsAfter - 1, destCol++, 1);
  361. // Null out the end of the last row
  362. wrappedLines[destLineIndex - 1]
  363. .erase(_emptyCellAttr, colsAfter - 1, colsAfter);
  364. }
  365. }
  366. }
  367. // Clear out remaining cells or fragments could remain;
  368. wrappedLines[destLineIndex].erase(_emptyCellAttr, destCol, colsAfter);
  369. // Work backwards and remove any rows at the end that only contain null cells
  370. int countToRemove = 0;
  371. for (int ix = wrappedLines.length - 1; ix > 0; ix--) {
  372. if (ix > destLineIndex || wrappedLines[ix].getTrimmedLength() == 0) {
  373. countToRemove++;
  374. } else {
  375. break;
  376. }
  377. }
  378. if (countToRemove > 0) {
  379. toRemove.add(y + wrappedLines.length - countToRemove); // index
  380. toRemove.add(countToRemove);
  381. }
  382. y += wrappedLines.length - 1;
  383. }
  384. return toRemove;
  385. }
  386. int _getWrappedLineTrimmedLengthRow(
  387. List<BufferLine> lines, int row, int cols) {
  388. return _getWrappedLineTrimmedLength(
  389. lines[row], row == lines.length - 1 ? null : lines[row + 1], cols);
  390. }
  391. int _getWrappedLineTrimmedLength(
  392. BufferLine line, BufferLine? nextLine, int cols) {
  393. // If this is the last row in the wrapped line, get the actual trimmed length
  394. if (nextLine == null) {
  395. return line.getTrimmedLength();
  396. }
  397. // Detect whether the following line starts with a wide character and the end of the current line
  398. // is null, if so then we can be pretty sure the null character should be excluded from the line
  399. // length]
  400. bool endsInNull =
  401. !(line.hasContentAt(cols - 1)) && line.getWidthAt(cols - 1) == 1;
  402. bool followingLineStartsWithWide = nextLine.getWidthAt(0) == 2;
  403. if (endsInNull && followingLineStartsWithWide) {
  404. return cols - 1;
  405. }
  406. return cols;
  407. }
  408. }