Quellcode durchsuchen

adapts reflow logic to CircularList

Reuses more from the port origin (xtermsharp) to better reflect the CircularList usage (in contrast to the List usage before)
devmil vor 4 Jahren
Ursprung
Commit
ab394bfd3f

+ 10 - 4
lib/buffer/buffer.dart

@@ -1,7 +1,8 @@
 import 'dart:math' show max, min;
 
 import 'package:xterm/buffer/buffer_line.dart';
-import 'package:xterm/buffer/buffer_reflow.dart';
+import 'package:xterm/buffer/reflow_strategy_narrower.dart';
+import 'package:xterm/buffer/reflow_strategy_wider.dart';
 import 'package:xterm/terminal/charset.dart';
 import 'package:xterm/terminal/terminal.dart';
 import 'package:xterm/utli/circular_list.dart';
@@ -486,10 +487,13 @@ class Buffer {
     if (newWidth > oldWidth) {
       lines.forEach((item, index) {
         item?.ensure(newWidth);
-      }, true);
+      });
     }
 
     if (newHeight > oldHeight) {
+      while (lines.length < newHeight) {
+        lines.push(_newEmptyLine());
+      }
       // Grow larger
       for (var i = 0; i < newHeight - oldHeight; i++) {
         if (_cursorY < oldHeight - 1) {
@@ -514,8 +518,10 @@ class Buffer {
     _cursorY = _cursorY.clamp(0, newHeight - 1);
 
     if (!terminal.isUsingAltBuffer()) {
-      final rf = BufferReflow(this);
-      rf.doReflow(oldWidth, newWidth);
+      final reflowStrategy = newWidth > oldWidth
+          ? ReflowStrategyWider(this)
+          : ReflowStrategyNarrower(this);
+      reflowStrategy.reflow(newWidth, newHeight, oldWidth, oldHeight);
     }
   }
 

+ 2 - 0
lib/buffer/buffer_line.dart

@@ -213,6 +213,8 @@ class BufferLine {
     final srcOffset = srcCol * _cellSize;
     final byteLen = len * _cellSize;
 
+    ensure(dstCol + len);
+
     final srcCopyView = src._cells.buffer.asUint8List(srcOffset, byteLen);
 
     _cells.buffer.asUint8List().setAll(dstOffset, srcCopyView);

+ 0 - 435
lib/buffer/buffer_reflow.dart

@@ -1,435 +0,0 @@
-import 'dart:math';
-
-import 'buffer.dart';
-import 'buffer_line.dart';
-
-class LayoutResult {
-  LayoutResult(this.layout, this.removedCount);
-
-  final List<int> layout;
-  final int removedCount;
-}
-
-class InsertionSet {
-  InsertionSet({this.lines, this.start, this.isNull = false});
-
-  final List<BufferLine>? lines;
-  final int? start;
-  final bool isNull;
-
-  static InsertionSet nullValue = InsertionSet(isNull: true);
-}
-
-class BufferReflow {
-  BufferReflow(this._buffer);
-
-  final Buffer _buffer;
-
-  void doReflow(int colsBefore, int colsAfter) {
-    if (colsBefore == colsAfter) {
-      return;
-    }
-
-    if (colsAfter > colsBefore) {
-      //got larger
-      _reflowLarger(colsBefore, colsAfter);
-    } else {
-      //got smaller
-      _reflowSmaller(colsBefore, colsAfter);
-    }
-  }
-
-  void _reflowLarger(int colsBefore, int colsAfter) {
-    var toRemove = _reflowLargerGetLinesToRemove(colsBefore, colsAfter);
-    if (toRemove.length > 0) {
-      var newLayoutResult =
-          _reflowLargerCreateNewLayout(_buffer.lines, toRemove);
-      _reflowLargerApplyNewLayout(newLayoutResult.layout);
-      _reflowLargerAdjustViewport(
-          colsBefore, colsAfter, newLayoutResult.removedCount);
-    }
-  }
-
-  void _reflowSmaller(int colsBefore, int colsAfter) {
-    // Gather all BufferLines that need to be inserted into the Buffer here so that they can be
-    // batched up and only committed once
-    List<InsertionSet> toInsert = [];
-    int countToInsert = 0;
-
-    // Go backwards as many lines may be trimmed and this will avoid considering them
-    for (int y = _buffer.lines.length - 1; y >= 0; y--) {
-      // Check whether this line is a problem or not, if not skip it
-      BufferLine nextLine = _buffer.lines[y];
-      int lineLength = nextLine.getTrimmedLength(colsBefore);
-      if (!nextLine.isWrapped && lineLength <= colsAfter) {
-        continue;
-      }
-
-      // Gather wrapped lines and adjust y to be the starting line
-      List<BufferLine> wrappedLines = [];
-      wrappedLines.add(nextLine);
-      while (nextLine.isWrapped && y > 0) {
-        nextLine = _buffer.lines[--y];
-        wrappedLines.insert(0, nextLine);
-      }
-
-      // If these lines contain the cursor don't touch them, the program will handle fixing up
-      // wrapped lines with the cursor
-      final absoluteY = _buffer.cursorY + _buffer.scrollOffsetFromTop;
-
-      if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
-        continue;
-      }
-
-      int lastLineLength = wrappedLines.last.getTrimmedLength(colsBefore);
-      List<int> destLineLengths =
-          _getNewLineLengths(wrappedLines, colsBefore, colsAfter);
-      int linesToAdd = destLineLengths.length - wrappedLines.length;
-
-      // Add the new lines
-      List<BufferLine> newLines = [];
-      for (int i = 0; i < linesToAdd; i++) {
-        BufferLine newLine = BufferLine(isWrapped: true);
-        newLines.add(newLine);
-      }
-
-      if (newLines.length > 0) {
-        toInsert.add(InsertionSet(
-            start: y + wrappedLines.length + countToInsert, lines: newLines));
-
-        countToInsert += newLines.length;
-      }
-
-      newLines.forEach((l) => wrappedLines.add(l));
-
-      // Copy buffer data to new locations, this needs to happen backwards to do in-place
-      int destLineIndex =
-          destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);
-      int destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;
-      if (destCol == 0) {
-        destLineIndex--;
-        destCol = destLineLengths[destLineIndex];
-      }
-
-      int srcLineIndex = wrappedLines.length - linesToAdd - 1;
-      int srcCol = lastLineLength;
-      while (srcLineIndex >= 0) {
-        int cellsToCopy = min(srcCol, destCol);
-        wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex],
-            srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy);
-        destCol -= cellsToCopy;
-        if (destCol == 0) {
-          destLineIndex--;
-          if (destLineIndex >= 0) destCol = destLineLengths[destLineIndex];
-        }
-
-        srcCol -= cellsToCopy;
-        if (srcCol == 0) {
-          srcLineIndex--;
-          int wrappedLinesIndex = max(srcLineIndex, 0);
-          srcCol = _getWrappedLineTrimmedLengthRow(
-              wrappedLines, wrappedLinesIndex, colsBefore);
-        }
-      }
-
-      // Null out the end of the line ends if a wide character wrapped to the following line
-      for (int i = 0; i < wrappedLines.length; i++) {
-        if (destLineLengths[i] < colsAfter) {
-          wrappedLines[i].removeRange(destLineLengths[i], colsBefore);
-        }
-      }
-
-      _buffer.adjustSavedCursor(0, linesToAdd);
-      //TODO: maybe row count has to be handled here?
-    }
-
-    _rearrange(toInsert, countToInsert);
-  }
-
-  void _rearrange(List<InsertionSet> toInsert, int countToInsert) {
-    // Rearrange lines in the buffer if there are any insertions, this is done at the end rather
-    // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
-    // costly calls to CircularList.splice.
-    if (toInsert.length > 0) {
-      // Record original lines so they don't get overridden when we rearrange the list
-      List<BufferLine> originalLines = List<BufferLine>.from(_buffer.lines);
-      _buffer.lines.addAll(
-          List<BufferLine>.generate(countToInsert, (index) => BufferLine()));
-
-      int originalLinesLength = originalLines.length;
-
-      int originalLineIndex = originalLinesLength - 1;
-      int nextToInsertIndex = 0;
-      InsertionSet nextToInsert = toInsert[nextToInsertIndex];
-
-      //TODO: remove rows that now are "too much"
-
-      int countInsertedSoFar = 0;
-      for (int i = originalLinesLength + countToInsert - 1; i >= 0; i--) {
-        if (!nextToInsert.isNull &&
-            nextToInsert.start != null &&
-            nextToInsert.lines != null &&
-            nextToInsert.start! > originalLineIndex + countInsertedSoFar) {
-          // Insert extra lines here, adjusting i as needed
-          for (int nextI = nextToInsert.lines!.length - 1;
-              nextI >= 0;
-              nextI--) {
-            if (i < 0) {
-              // if we reflow and the content has to be scrolled back past the beginning
-              // of the buffer then we end up loosing those lines
-              break;
-            }
-
-            _buffer.lines[i--] = nextToInsert.lines![nextI];
-          }
-
-          i++;
-
-          countInsertedSoFar += nextToInsert.lines!.length;
-          if (nextToInsertIndex < toInsert.length - 1) {
-            nextToInsert = toInsert[++nextToInsertIndex];
-          } else {
-            nextToInsert = InsertionSet.nullValue; //TODO: just break?
-          }
-        } else {
-          _buffer.lines[i] = originalLines[originalLineIndex--];
-        }
-      }
-    }
-  }
-
-  /// <summary>
-  /// Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
-  /// compute the wrapping points since wide characters may need to be wrapped onto the following line.
-  /// This function will return an array of numbers of where each line wraps to, the resulting array
-  /// will only contain the values `newCols` (when the line does not end with a wide character) and
-  /// `newCols - 1` (when the line does end with a wide character), except for the last value which
-  /// will contain the remaining items to fill the line.
-  /// Calling this with a `newCols` value of `1` will lock up.
-  /// </summary>
-  List<int> _getNewLineLengths(
-      List<BufferLine> wrappedLines, int oldCols, int newCols) {
-    List<int> newLineLengths = [];
-
-    int cellsNeeded = 0;
-    for (int i = 0; i < wrappedLines.length; i++) {
-      cellsNeeded += _getWrappedLineTrimmedLengthRow(wrappedLines, i, oldCols);
-    }
-
-    // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
-    // linesNeeded
-    int srcCol = 0;
-    int srcLine = 0;
-    int cellsAvailable = 0;
-    while (cellsAvailable < cellsNeeded) {
-      if (cellsNeeded - cellsAvailable < newCols) {
-        // Add the final line and exit the loop
-        newLineLengths.add(cellsNeeded - cellsAvailable);
-        break;
-      }
-
-      srcCol += newCols;
-      int oldTrimmedLength =
-          _getWrappedLineTrimmedLengthRow(wrappedLines, srcLine, oldCols);
-      if (srcCol > oldTrimmedLength) {
-        srcCol -= oldTrimmedLength;
-        srcLine++;
-      }
-
-      bool endsWithWide = wrappedLines[srcLine].cellGetWidth(srcCol - 1) == 2;
-      if (endsWithWide) {
-        srcCol--;
-      }
-
-      int lineLength = endsWithWide ? newCols - 1 : newCols;
-      newLineLengths.add(lineLength);
-      cellsAvailable += lineLength;
-    }
-
-    return newLineLengths;
-  }
-
-  void _reflowLargerAdjustViewport(
-      int colsBefore, int colsAfter, int countRemoved) {
-    // Adjust viewport based on number of items removed
-    var viewportAdjustments = countRemoved;
-    while (viewportAdjustments-- > 0) {
-      //viewport is at the top
-      if (_buffer.lines.length <= _buffer.terminal.viewHeight) {
-        //cursor is not at the top
-        if (_buffer.cursorY > 0) {
-          _buffer.moveCursorY(-1);
-        }
-        //buffer doesn't have enough lines
-        if (_buffer.lines.length < _buffer.terminal.viewHeight) {
-          // Add an extra row at the bottom of the viewport
-          _buffer.lines.add(BufferLine());
-        }
-      }
-    }
-    //TODO: adjust buffer content to max length
-    _buffer.adjustSavedCursor(0, -countRemoved);
-  }
-
-  void _reflowLargerApplyNewLayout(List<int> newLayout) {
-    var newLayoutLines = List<BufferLine>.generate(
-        newLayout.length, (index) => _buffer.lines[newLayout[index]]);
-
-    // Rearrange the list
-    for (int i = 0; i < newLayoutLines.length; i++) {
-      _buffer.lines[i] = newLayoutLines[i];
-    }
-
-    _buffer.lines
-        .removeRange(newLayoutLines.length - 1, _buffer.lines.length - 1);
-  }
-
-  LayoutResult _reflowLargerCreateNewLayout(
-      List<BufferLine> lines, List<int> toRemove) {
-    var layout = List<int>.empty(growable: true);
-
-    // First iterate through the list and get the actual indexes to use for rows
-    int nextToRemoveIndex = 0;
-    int nextToRemoveStart = toRemove[nextToRemoveIndex];
-    int countRemovedSoFar = 0;
-
-    for (int i = 0; i < lines.length; i++) {
-      if (nextToRemoveStart == i) {
-        int countToRemove = toRemove[++nextToRemoveIndex];
-
-        i += countToRemove - 1;
-        countRemovedSoFar += countToRemove;
-
-        nextToRemoveStart = lines.length + 1;
-        if (nextToRemoveIndex < toRemove.length - 1)
-          nextToRemoveStart = toRemove[++nextToRemoveIndex];
-      } else {
-        layout.add(i);
-      }
-    }
-
-    return LayoutResult(layout, countRemovedSoFar);
-  }
-
-  List<int> _reflowLargerGetLinesToRemove(int colsBefore, int colsAfter) {
-    List<int> toRemove = List<int>.empty(growable: true);
-
-    for (int y = 0; y < _buffer.lines.length - 1; y++) {
-      // Check if this row is wrapped
-      int i = y;
-      BufferLine nextLine = _buffer.lines[++i];
-      if (!nextLine.isWrapped) {
-        continue;
-      }
-
-      // Check how many lines it's wrapped for
-      List<BufferLine> wrappedLines = List<BufferLine>.empty(growable: true);
-      wrappedLines.add(_buffer.lines[y]);
-      while (i < _buffer.lines.length && nextLine.isWrapped) {
-        wrappedLines.add(nextLine);
-        nextLine = _buffer.lines[++i];
-      }
-
-      final bufferAbsoluteY = _buffer.cursorY + _buffer.scrollOffsetFromTop;
-
-      // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
-      // lines with the cursor
-      if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
-        y += wrappedLines.length - 1;
-        continue;
-      }
-
-      // Copy buffer data to new locations
-      int destLineIndex = 0;
-      int destCol = _getWrappedLineTrimmedLengthRow(
-          _buffer.lines, destLineIndex, colsBefore);
-      int srcLineIndex = 1;
-      int srcCol = 0;
-      while (srcLineIndex < wrappedLines.length) {
-        int srcTrimmedTineLength = _getWrappedLineTrimmedLengthRow(
-            wrappedLines, srcLineIndex, colsBefore);
-        int srcRemainingCells = srcTrimmedTineLength - srcCol;
-        int destRemainingCells = colsAfter - destCol;
-        int cellsToCopy = min(srcRemainingCells, destRemainingCells);
-
-        wrappedLines[destLineIndex].copyCellsFrom(
-            wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy);
-
-        destCol += cellsToCopy;
-        if (destCol == colsAfter) {
-          destLineIndex++;
-          destCol = 0;
-        }
-
-        srcCol += cellsToCopy;
-        if (srcCol == srcTrimmedTineLength) {
-          srcLineIndex++;
-          srcCol = 0;
-        }
-
-        // Make sure the last cell isn't wide, if it is copy it to the current dest
-        if (destCol == 0 && destLineIndex != 0) {
-          if (wrappedLines[destLineIndex - 1].cellGetWidth(colsAfter - 1) ==
-              2) {
-            wrappedLines[destLineIndex].copyCellsFrom(
-                wrappedLines[destLineIndex - 1], colsAfter - 1, destCol++, 1);
-            // Null out the end of the last row
-            wrappedLines[destLineIndex - 1].erase(
-                _buffer.terminal.cursor, colsAfter - 1, colsAfter, false);
-          }
-        }
-      }
-
-      // Clear out remaining cells or fragments could remain;
-      wrappedLines[destLineIndex]
-          .erase(_buffer.terminal.cursor, destCol, colsAfter, false);
-
-      // Work backwards and remove any rows at the end that only contain null cells
-      int countToRemove = 0;
-      for (int ix = wrappedLines.length - 1; ix > 0; ix--) {
-        if (ix > destLineIndex ||
-            wrappedLines[ix].getTrimmedLength(colsBefore) == 0) {
-          countToRemove++;
-        } else {
-          break;
-        }
-      }
-
-      if (countToRemove > 0) {
-        toRemove.add(y + wrappedLines.length - countToRemove); // index
-        toRemove.add(countToRemove);
-      }
-
-      y += wrappedLines.length - 1;
-    }
-
-    return toRemove;
-  }
-
-  int _getWrappedLineTrimmedLengthRow(
-      List<BufferLine> lines, int row, int cols) {
-    return _getWrappedLineTrimmedLength(
-        lines[row], row == lines.length - 1 ? null : lines[row + 1], cols);
-  }
-
-  int _getWrappedLineTrimmedLength(
-      BufferLine line, BufferLine? nextLine, int cols) {
-    // If this is the last row in the wrapped line, get the actual trimmed length
-    if (nextLine == null) {
-      return line.getTrimmedLength(cols);
-    }
-
-    // Detect whether the following line starts with a wide character and the end of the current line
-    // is null, if so then we can be pretty sure the null character should be excluded from the line
-    // length]
-    bool endsInNull =
-        !(line.cellHasContent(cols - 1)) && line.cellGetWidth(cols - 1) == 1;
-    bool followingLineStartsWithWide = nextLine.cellGetWidth(0) == 2;
-
-    if (endsInNull && followingLineStartsWithWide) {
-      return cols - 1;
-    }
-
-    return cols;
-  }
-}

+ 47 - 0
lib/buffer/reflow_strategy.dart

@@ -0,0 +1,47 @@
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/utli/circular_list.dart';
+
+abstract class ReflowStrategy {
+  final Buffer _buffer;
+
+  ReflowStrategy(this._buffer);
+
+  Buffer get buffer => _buffer;
+
+  void reflow(int newCols, int newRows, int oldCols, int oldRows);
+
+  static int getWrappedLineTrimmedLengthFromCircularList(
+      CircularList<BufferLine> lines, int row, int cols) {
+    return getWrappedLineTrimmedLengthFromLine(
+        lines[row], row == lines.length - 1 ? null : lines[row + 1], cols);
+  }
+
+  static int getWrappedLineTrimmedLengthFromLines(
+      List<BufferLine> lines, int row, int cols) {
+    return getWrappedLineTrimmedLengthFromLine(
+        lines[row], row == lines.length - 1 ? null : lines[row + 1], cols);
+  }
+
+  static int getWrappedLineTrimmedLengthFromLine(
+      BufferLine? line, BufferLine? nextLine, int cols) {
+    if (line == null) {
+      return 0;
+    }
+    if (nextLine == null) {
+      return line.getTrimmedLength(cols);
+    }
+
+    // Detect whether the following line starts with a wide character and the end of the current line
+    // is null, if so then we can be pretty sure the null character should be excluded from the line
+    // length]
+    final endsInNull =
+        !(line.cellHasContent(cols - 1)) && line.cellGetWidth(cols - 1) == 1;
+    final followingLineStartsWithWide = nextLine.cellGetWidth(0) == 2;
+
+    if (endsInNull && followingLineStartsWithWide) {
+      return cols - 1;
+    }
+    return cols;
+  }
+}

+ 247 - 0
lib/buffer/reflow_strategy_narrower.dart

@@ -0,0 +1,247 @@
+import 'dart:math';
+
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/reflow_strategy.dart';
+import 'package:xterm/utli/circular_list.dart';
+
+class ReflowStrategyNarrower extends ReflowStrategy {
+  ReflowStrategyNarrower(Buffer buffer) : super(buffer);
+
+  @override
+  void reflow(int newCols, int newRows, int oldCols, int oldRows) {
+    // Gather all BufferLines that need to be inserted into the Buffer here so that they can be
+    // batched up and only committed once
+    List<InsertionSet> toInsert = List<InsertionSet>.empty(growable: true);
+    var countToInsert = 0;
+
+    // Go backwards as many lines may be trimmed and this will avoid considering them
+    for (int y = buffer.lines.length - 1; y >= 0; y--) {
+      // Check whether this line is a problem or not, if not skip it
+      BufferLine nextLine = buffer.lines[y]!;
+      int lineLength = nextLine.getTrimmedLength(oldCols);
+      if (!nextLine.isWrapped && lineLength <= newCols) {
+        continue;
+      }
+
+      // Gather wrapped lines and adjust y to be the starting line
+      final wrappedLines = List<BufferLine>.empty(growable: true);
+      wrappedLines.add(nextLine);
+      while (nextLine.isWrapped && y > 0) {
+        nextLine = buffer.lines[--y]!;
+        wrappedLines.insert(0, nextLine);
+      }
+
+      // If these lines contain the cursor don't touch them, the program will handle fixing up
+      // wrapped lines with the cursor
+      final absoluteY = buffer.cursorY + buffer.scrollOffsetFromTop;
+
+      if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
+        continue;
+      }
+
+      int lastLineLength = wrappedLines.last.getTrimmedLength(oldCols);
+      final destLineLengths =
+          _getNewLineLengths(wrappedLines, oldCols, newCols);
+      int linesToAdd = destLineLengths.length - wrappedLines.length;
+
+      // Add the new lines
+      final newLines = List<BufferLine>.empty(growable: true);
+      for (int i = 0; i < linesToAdd; i++) {
+        BufferLine newLine = BufferLine(isWrapped: true);
+        newLines.add(newLine);
+      }
+
+      if (newLines.length > 0) {
+        toInsert.add(InsertionSet()
+          ..start = y + wrappedLines.length + countToInsert
+          ..lines = List<BufferLine>.from(newLines));
+
+        countToInsert += newLines.length;
+      }
+
+      newLines.forEach((l) => wrappedLines.add(l));
+
+      // Copy buffer data to new locations, this needs to happen backwards to do in-place
+      int destLineIndex =
+          destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);
+      int destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;
+      if (destCol == 0) {
+        destLineIndex--;
+        destCol = destLineLengths[destLineIndex];
+      }
+
+      int srcLineIndex = wrappedLines.length - linesToAdd - 1;
+      int srcCol = lastLineLength;
+      while (srcLineIndex >= 0) {
+        int cellsToCopy = min(srcCol, destCol);
+        wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex],
+            srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy);
+        destCol -= cellsToCopy;
+        if (destCol == 0) {
+          destLineIndex--;
+          if (destLineIndex >= 0) destCol = destLineLengths[destLineIndex];
+        }
+
+        srcCol -= cellsToCopy;
+        if (srcCol == 0) {
+          srcLineIndex--;
+          int wrappedLinesIndex = max(srcLineIndex, 0);
+          srcCol = ReflowStrategy.getWrappedLineTrimmedLengthFromLines(
+              wrappedLines, wrappedLinesIndex, oldCols);
+        }
+      }
+
+      // Null out the end of the line ends if a wide character wrapped to the following line
+      for (int i = 0; i < wrappedLines.length; i++) {
+        if (destLineLengths[i] < newCols) {
+          wrappedLines[i].removeRange(destLineLengths[i], oldCols);
+        }
+      }
+
+      buffer.adjustSavedCursor(0, linesToAdd);
+    }
+
+    rearrange(toInsert, countToInsert);
+  }
+
+  void rearrange(List<InsertionSet> toInsert, int countToInsert) {
+    // Rearrange lines in the buffer if there are any insertions, this is done at the end rather
+    // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
+    // costly calls to CircularList.splice.
+    if (toInsert.length > 0) {
+      // Record buffer insert events and then play them back backwards so that the indexes are
+      // correct
+      List<int> insertEvents = List<int>.empty(growable: true);
+
+      // Record original lines so they don't get overridden when we rearrange the list
+      CircularList<BufferLine> originalLines =
+          new CircularList<BufferLine>(buffer.lines.maxLength);
+      for (int i = 0; i < buffer.lines.length; i++) {
+        originalLines.push(buffer.lines[i]!);
+      }
+
+      int originalLinesLength = buffer.lines.length;
+
+      int originalLineIndex = originalLinesLength - 1;
+      int nextToInsertIndex = 0;
+      var nextToInsert = toInsert[nextToInsertIndex];
+      buffer.lines.length =
+          min(buffer.lines.maxLength, buffer.lines.length + countToInsert);
+
+      int countInsertedSoFar = 0;
+      for (int i = min(buffer.lines.maxLength - 1,
+              originalLinesLength + countToInsert - 1);
+          i >= 0;
+          i--) {
+        if (!nextToInsert.isNull &&
+            nextToInsert.start > originalLineIndex + countInsertedSoFar) {
+          // Insert extra lines here, adjusting i as needed
+          for (int nextI = nextToInsert.lines!.length - 1;
+              nextI >= 0;
+              nextI--) {
+            if (i < 0) {
+              // if we reflow and the content has to be scrolled back past the beginning
+              // of the buffer then we end up loosing those lines
+              break;
+            }
+
+            buffer.lines[i--] = nextToInsert.lines![nextI];
+          }
+
+          i++;
+
+          // Create insert events for later
+          //insertEvents.Add ({
+          //	index: originalLineIndex + 1,
+          //	amount: nextToInsert.newLines.length
+          //});
+
+          countInsertedSoFar += nextToInsert.lines!.length;
+          if (nextToInsertIndex < toInsert.length - 1) {
+            nextToInsert = toInsert[++nextToInsertIndex];
+          } else {
+            nextToInsert = InsertionSet.nul;
+          }
+        } else {
+          buffer.lines[i] = originalLines[originalLineIndex--];
+        }
+      }
+
+      /*
+				// Update markers
+				let insertCountEmitted = 0;
+				for (let i = insertEvents.length - 1; i >= 0; i--) {
+					insertEvents [i].index += insertCountEmitted;
+					this.lines.onInsertEmitter.fire (insertEvents [i]);
+					insertCountEmitted += insertEvents [i].amount;
+				}
+				const amountToTrim = Math.max (0, originalLinesLength + countToInsert - this.lines.maxLength);
+				if (amountToTrim > 0) {
+					this.lines.onTrimEmitter.fire (amountToTrim);
+				}
+				*/
+    }
+  }
+
+  /// <summary>
+  /// Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
+  /// compute the wrapping points since wide characters may need to be wrapped onto the following line.
+  /// This function will return an array of numbers of where each line wraps to, the resulting array
+  /// will only contain the values `newCols` (when the line does not end with a wide character) and
+  /// `newCols - 1` (when the line does end with a wide character), except for the last value which
+  /// will contain the remaining items to fill the line.
+  /// Calling this with a `newCols` value of `1` will lock up.
+  /// </summary>
+  List<int> _getNewLineLengths(
+      List<BufferLine> wrappedLines, int oldCols, int newCols) {
+    final newLineLengths = List<int>.empty(growable: true);
+
+    int cellsNeeded = 0;
+    for (int i = 0; i < wrappedLines.length; i++) {
+      cellsNeeded += ReflowStrategy.getWrappedLineTrimmedLengthFromLines(
+          wrappedLines, i, oldCols);
+    }
+
+    // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
+    // linesNeeded
+    int srcCol = 0;
+    int srcLine = 0;
+    int cellsAvailable = 0;
+    while (cellsAvailable < cellsNeeded) {
+      if (cellsNeeded - cellsAvailable < newCols) {
+        // Add the final line and exit the loop
+        newLineLengths.add(cellsNeeded - cellsAvailable);
+        break;
+      }
+
+      srcCol += newCols;
+      int oldTrimmedLength =
+          ReflowStrategy.getWrappedLineTrimmedLengthFromLines(
+              wrappedLines, srcLine, oldCols);
+      if (srcCol > oldTrimmedLength) {
+        srcCol -= oldTrimmedLength;
+        srcLine++;
+      }
+
+      bool endsWithWide = wrappedLines[srcLine].cellGetWidth(srcCol - 1) == 2;
+      if (endsWithWide) {
+        srcCol--;
+      }
+
+      int lineLength = endsWithWide ? newCols - 1 : newCols;
+      newLineLengths.add(lineLength);
+      cellsAvailable += lineLength;
+    }
+
+    return newLineLengths;
+  }
+}
+
+class InsertionSet {
+  List<BufferLine>? lines;
+  int start = 0;
+  bool isNull = false;
+
+  static InsertionSet nul = InsertionSet()..isNull = true;
+}

+ 202 - 0
lib/buffer/reflow_strategy_wider.dart

@@ -0,0 +1,202 @@
+import 'dart:math';
+
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/reflow_strategy.dart';
+import 'package:xterm/utli/circular_list.dart';
+
+class ReflowStrategyWider extends ReflowStrategy {
+  ReflowStrategyWider(Buffer buffer) : super(buffer);
+
+  @override
+  void reflow(int newCols, int newRows, int oldCols, int oldRows) {
+    final toRemove = _getLinesToRemove(buffer.lines, oldCols, newCols);
+    if (toRemove.length > 0) {
+      final newLayoutResult = _createNewLayout(buffer.lines, toRemove);
+      _applyNewLayout(buffer.lines, newLayoutResult.layout);
+      _adjustViewport(newCols, newRows, newLayoutResult.removedCount);
+    }
+  }
+
+  /// <summary>
+  /// Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
+  /// when a wrapped line unwraps.
+  /// </summary>
+  /// <param name="lines">The buffer lines</param>
+  /// <param name="oldCols">The columns before resize</param>
+  /// <param name="newCols">The columns after resize</param>
+  /// <param name="bufferAbsoluteY"></param>
+  /// <param name="nullCharacter"></param>
+  List<int> _getLinesToRemove(
+      CircularList<BufferLine> lines, int oldCols, int newCols) {
+    // Gather all BufferLines that need to be removed from the Buffer here so that they can be
+    // batched up and only committed once
+    final toRemove = List<int>.empty(growable: true);
+
+    for (int y = 0; y < lines.length - 1; y++) {
+      // Check if this row is wrapped
+      int i = y;
+      BufferLine nextLine = lines[++i]!;
+      if (!nextLine.isWrapped) {
+        continue;
+      }
+
+      // Check how many lines it's wrapped for
+      final wrappedLines = List<BufferLine>.empty(growable: true);
+      wrappedLines.add(lines[y]!);
+      while (i < lines.length && nextLine.isWrapped) {
+        wrappedLines.add(nextLine);
+        nextLine = lines[++i]!;
+      }
+
+      final bufferAbsoluteY = buffer.cursorY + buffer.scrollOffsetFromTop;
+
+      // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
+      // lines with the cursor
+      if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
+        y += wrappedLines.length - 1;
+        continue;
+      }
+
+      // Copy buffer data to new locations
+      int destLineIndex = 0;
+      int destCol = ReflowStrategy.getWrappedLineTrimmedLengthFromCircularList(
+          buffer.lines, destLineIndex, oldCols);
+      int srcLineIndex = 1;
+      int srcCol = 0;
+      while (srcLineIndex < wrappedLines.length) {
+        int srcTrimmedTineLength =
+            ReflowStrategy.getWrappedLineTrimmedLengthFromLines(
+                wrappedLines, srcLineIndex, oldCols);
+        int srcRemainingCells = srcTrimmedTineLength - srcCol;
+        int destRemainingCells = newCols - destCol;
+        int cellsToCopy = min(srcRemainingCells, destRemainingCells);
+
+        wrappedLines[destLineIndex].copyCellsFrom(
+            wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy);
+
+        destCol += cellsToCopy;
+        if (destCol == newCols) {
+          destLineIndex++;
+          destCol = 0;
+        }
+
+        srcCol += cellsToCopy;
+        if (srcCol == srcTrimmedTineLength) {
+          srcLineIndex++;
+          srcCol = 0;
+        }
+
+        // Make sure the last cell isn't wide, if it is copy it to the current dest
+        if (destCol == 0 && destLineIndex != 0) {
+          if (wrappedLines[destLineIndex - 1].cellGetWidth(newCols - 1) == 2) {
+            wrappedLines[destLineIndex].copyCellsFrom(
+                wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1);
+            // Null out the end of the last row
+            wrappedLines[destLineIndex - 1]
+                .erase(buffer.terminal.cursor, newCols - 1, newCols, false);
+          }
+        }
+      }
+
+      // Clear out remaining cells or fragments could remain;
+      wrappedLines[destLineIndex]
+          .erase(buffer.terminal.cursor, destCol, newCols);
+
+      // Work backwards and remove any rows at the end that only contain null cells
+      int countToRemove = 0;
+      for (int ix = wrappedLines.length - 1; ix > 0; ix--) {
+        if (ix > destLineIndex ||
+            wrappedLines[ix].getTrimmedLength(oldCols) == 0) {
+          countToRemove++;
+        } else {
+          break;
+        }
+      }
+
+      if (countToRemove > 0) {
+        toRemove.add(y + wrappedLines.length - countToRemove); // index
+        toRemove.add(countToRemove);
+      }
+
+      y += wrappedLines.length - 1;
+    }
+
+    return toRemove;
+  }
+
+  LayoutResult _createNewLayout(
+      CircularList<BufferLine> lines, List<int> toRemove) {
+    var layout = new CircularList<int>(lines.length);
+
+    // First iterate through the list and get the actual indexes to use for rows
+    int nextToRemoveIndex = 0;
+    int nextToRemoveStart = toRemove[nextToRemoveIndex];
+    int countRemovedSoFar = 0;
+
+    for (int i = 0; i < lines.length; i++) {
+      if (nextToRemoveStart == i) {
+        int countToRemove = toRemove[++nextToRemoveIndex];
+
+        // Tell markers that there was a deletion
+        //lines.onDeleteEmitter.fire ({
+        //	index: i - countRemovedSoFar,
+        //	amount: countToRemove
+        //});
+
+        i += countToRemove - 1;
+        countRemovedSoFar += countToRemove;
+
+        nextToRemoveStart = lines.length + 1; //was: int.max
+        if (nextToRemoveIndex < toRemove.length - 1)
+          nextToRemoveStart = toRemove[++nextToRemoveIndex];
+      } else {
+        layout.push(i);
+      }
+    }
+
+    return new LayoutResult(layout, countRemovedSoFar);
+  }
+
+  void _applyNewLayout(
+      CircularList<BufferLine> lines, CircularList<int> newLayout) {
+    var newLayoutLines = new CircularList<BufferLine>(lines.length);
+
+    for (int i = 0; i < newLayout.length; i++) {
+      newLayoutLines.push(lines[newLayout[i]!]!);
+    }
+
+    // Rearrange the list
+    for (int i = 0; i < newLayoutLines.length; i++) {
+      lines[i] = newLayoutLines[i];
+    }
+
+    lines.length = newLayout.length;
+  }
+
+  void _adjustViewport(int newCols, int newRows, int countRemoved) {
+    int viewportAdjustments = countRemoved;
+    while (viewportAdjustments-- > 0) {
+      if (buffer.lines.length <= buffer.terminal.viewHeight) {
+        //cursor is not at the top
+        if (buffer.cursorY > 0) {
+          buffer.moveCursorY(-1);
+        }
+        //buffer doesn't have enough lines
+        if (buffer.lines.length < buffer.terminal.viewHeight) {
+          // Add an extra row at the bottom of the viewport
+          buffer.lines.push(BufferLine());
+        }
+      }
+    }
+
+    buffer.adjustSavedCursor(0, -countRemoved);
+  }
+}
+
+class LayoutResult {
+  CircularList<int> layout;
+  int removedCount;
+
+  LayoutResult(this.layout, this.removedCount);
+}

+ 8 - 3
lib/utli/circular_list.dart

@@ -47,9 +47,8 @@ class CircularList<T> {
     _length = value;
   }
 
-  void forEach(void Function(T? item, int index) callback,
-      [bool includeBuffer = false]) {
-    final len = includeBuffer ? _array.length : _length;
+  void forEach(void Function(T? item, int index) callback) {
+    final len = _length;
     for (int i = 0; i < len; i++) {
       callback(_array[_getCyclicIndex(i)], i);
     }
@@ -68,6 +67,12 @@ class CircularList<T> {
     _length = 0;
   }
 
+  void pushAll(Iterable<T> items) {
+    items.forEach((element) {
+      push(element);
+    });
+  }
+
   void push(T value) {
     _array[_getCyclicIndex(_length)] = value;
     if (_length == _array.length) {