@@ -156,7 +156,7 @@ class LineEditor {
156156 this . ghostLines = 0 ; // how many logical lines those rows are the rows of, and how many cells they hold: what a wider
157157 this . ghostCells = 0 ; // panel can join them to (at least this many lines, and no fewer rows than the cells need)
158158 this . blank = 0 ; // rows known to be blank under what the region takes of the screen (a lower bound; see resize)
159- this . span = 1 ; // the rows the region has on the screen, as the terminal has them until it is drawn again
159+ this . span = 0 ; // the rows the region has on the screen, as the terminal has them until it is drawn again
160160 this . targetRow = 0 ; // the row the cursor was left on, in region rows
161161 this . phantom = false ; // the cursor is on a row made by forcing a wrap
162162 this . fresh = false ; // the next draw starts a prompt: write the prompt marks with it
@@ -317,9 +317,10 @@ class LineEditor {
317317 if ( target . col > 0 ) { out += CSI + target . col + 'C' ; }
318318
319319 // What the region takes of the blank rows under it is not blank any more, and what it gives back (it got shorter: the
320- // rows were erased) is; a region that fills the panel leaves none, and Ctrl+L puts it at the top of the screen.
320+ // rows were erased) is: the rows from where it begins to the bottom of the panel are the rows it had and the blank
321+ // ones under them (Ctrl+L puts it at the top of an empty screen, which is no fewer).
321322 const shown = written - winTop + 1 ;
322- this . blank = shown >= this . rows ? 0 : clear ? this . rows - shown : Math . max ( 0 , this . blank + this . span - shown ) ;
323+ this . blank = Math . max ( 0 , this . blank + this . span - shown ) ;
323324 this . span = shown ;
324325 this . winTop = winTop ;
325326 this . cursorRow = target . row - winTop ;
@@ -345,11 +346,10 @@ class LineEditor {
345346 this . phantom = false ;
346347 }
347348
348- /** Nothing is known about the rows under what is drawn next, and it is not the region that was: one row, as at a prompt . */
349+ /** A new prompt: nothing is known of the rows under what is drawn next, and none of the region that was is a row of it . */
349350 _noRegion ( ) {
350351 this . blank = 0 ;
351- this . span = 1 ;
352- this . lastRow = this . winTop ;
352+ this . span = 0 ;
353353 }
354354
355355 /** The rows that were pushed into the scrollback are history from here on. */
@@ -399,14 +399,20 @@ class LineEditor {
399399 * ghostLines the logical lines those rows are rows of (a wider panel joins the rows of one line, never two lines,
400400 * so each stays at least one row);
401401 * ghostCells the cells those rows hold (joined, they need at least ceil(cells / width) rows).
402- * Lines and cells are counted when the rows are pushed and only ever from the rows xterm really pushed: the rows the
403- * screen had at that moment (`_headRows` of the arrangement `_reflowRows` works out), so a row is never credited with
404- * more cells than it holds. Counted afterwards, at the width the panel is asked about, they were: a row xterm pushed
405- * is as wide as the screen was (24 cells, pushed when the panel was 24 wide; and the line of the cursor is never
406- * re-wrapped, so a wider panel does not make it 64), which is what made two rows into one at 104 columns and the
407- * redraw start a row too high. Pulled rows are the newest: what is left is at least as many lines, and as many cells,
408- * as the rows that came back could have held. A narrower panel only splits rows, which makes the floor lower, not
409- * wrong. `_noGhosts` forgets all three: they say nothing of rows that are no longer there.
402+ * Why none of them can be more than there is:
403+ * - the cells are counted when the rows are pushed, from the rows xterm really pushed (`_headRows` of what
404+ * `_reflowRows` works out): a row is credited with the cells it holds, not with what a re-wrap at the width asked
405+ * about would give. A row xterm pushed is as wide as the screen was (24 cells, pushed when the panel was 24 wide),
406+ * and the line of the cursor is not re-wrapped (so a wider panel does not make it 64): counted afterwards at the
407+ * new width they were 105 cells in two rows that join to one at 104 columns, and the redraw started a row high;
408+ * - lines and cells are only counted from rows that were at or above the cursor (`_headRows`: xterm makes room for
409+ * the rows a line gains by dropping rows from the bottom of its buffer, and what is left of a line under the cursor
410+ * is flagged as a continuation of the row above it);
411+ * - what is pulled back is the newest of them, so what is left is at least the lines and the cells of the rest
412+ * (a row holds at most as many cells as the wider of the two widths); a narrower panel only splits rows, which
413+ * leaves the floor low, not wrong; and rows are not cut afterwards, because the editor's own redraw begins every
414+ * window with a row that is a line of its own for xterm, so they are not the line of the cursor any more;
415+ * - a new prompt, Ctrl+L and Ctrl+C forget all three (`_noGhosts`): they say nothing of rows that are not there.
410416 * @param {number } cols
411417 * @param {number } rows
412418 */
@@ -515,19 +521,23 @@ class LineEditor {
515521 * scrollback. `arranged` is what `_reflowRows` made of the screen (one arrangement per hypothesis about the blank
516522 * rows under the region): the rows as the re-wrapping left them, line by line, and the first row of a window is a
517523 * line of its own (see `_reflowRows`). The least of the arrangements is taken: each is a floor, and the one that is
518- * the truth is one of them.
519- * @param {Array<Array<{ line: number, cells: number }>> } arranged
524+ * the truth is one of them. Only the rows that were at or above the cursor are looked at (`safe`): xterm makes room
525+ * for the rows a line gains by dropping rows from the BOTTOM of its buffer, the blank ones and, when there are not
526+ * enough of them, the rows of the lines under the cursor, and what is left of such a line is the end of it, flagged as
527+ * a continuation of whatever row is above it (a wrapped line that begins with a continuation row has no first row, and
528+ * its first pushed row is one line with the row before it). Rows that were above the cursor are never dropped.
529+ * @param {Array<{ rows: Array<{ line: number, cells: number }>, safe: number }> } arranged
520530 * @param {number } count
521531 * @returns {{ lines: number, cells: number } }
522532 */
523533 _headRows ( arranged , count ) {
524534 let lines = Infinity ;
525535 let cells = Infinity ;
526- for ( const rows of arranged ) {
536+ for ( const { rows, safe } of arranged ) {
527537 let seen = 0 ;
528538 let held = 0 ;
529539 let line = - 1 ;
530- for ( let i = 0 ; i < count && i < rows . length ; i ++ ) {
540+ for ( let i = 0 ; i < count && i < safe && i < rows . length ; i ++ ) {
531541 if ( rows [ i ] . line !== line ) { seen ++ ; line = rows [ i ] . line ; }
532542 held += rows [ i ] . cells ;
533543 }
@@ -570,32 +580,42 @@ class LineEditor {
570580 * lower of two answers left).
571581 * @param {number } cols the new width
572582 * @param {number } rows the new height
573- * @returns {{ row: number, added: number, removed: number, span: number, arranged: Array<Array<{ line: number, cells: number }>> } }
583+ * @returns {{ row: number, added: number, removed: number, span: number, arranged: Array<{ rows: Array<{ line: number, cells: number }>, safe: number } > } }
574584 * the row, the rows the re-wrapping added and took out, how many rows the region has on the screen afterwards (at
575- * least), and the rows the screen has
585+ * least), and the rows the screen has (one arrangement per hypothesis)
576586 */
577587 _cursorRowAfterReflow ( cols , rows ) {
578- if ( rows >= this . rows ) {
579- const whole = this . _reflowRows ( cols , this . lastRow ) ;
580- return { row : whole . row , added : whole . added , removed : whole . removed , span : whole . rows . length , arranged : [ whole . rows ] } ;
581- }
582- const lost = this . rows - rows ;
583- // the most blank rows there can be under the region on the screen (a window fills it), and the fewest
588+ // (a panel that is not shorter has lost nothing: both ends are then the whole region)
589+ const lost = Math . max ( 0 , this . rows - rows ) ;
590+ // the fewest blank rows there are under the region (`blank`: a floor), and the most there can be (a window fills the
591+ // screen, and a region on a screen that has not scrolled is at the top of it)
584592 const most = Math . max ( 0 , this . rows - 1 - ( this . lastRow - this . winTop ) ) ;
585- const least = Math . min ( this . blank , most ) ;
586- const cut = this . _reflowRows ( cols , Math . max ( this . targetRow , this . lastRow - Math . max ( 0 , lost - least ) ) ) ;
587- const kept = this . _reflowRows ( cols , Math . max ( this . targetRow , this . lastRow - Math . max ( 0 , lost - most ) ) ) ;
588- const lower = kept . row < cut . row ? kept : cut ;
589- return { row : lower . row , added : lower . added , removed : lower . removed , span : cut . rows . length , arranged : [ cut . rows , kept . rows ] } ;
593+ const loBottom = Math . max ( this . targetRow , this . lastRow - Math . max ( 0 , lost - this . blank ) ) ;
594+ const hiBottom = Math . max ( this . targetRow , this . lastRow - Math . max ( 0 , lost - most ) ) ;
595+ const cut = this . _reflowRows ( cols , loBottom ) ;
596+ const kept = hiBottom === loBottom ? cut : this . _reflowRows ( cols , hiBottom ) ;
597+ // the true number of rows that were dropped is anywhere between the two ends, and the row the cursor ends on is not
598+ // monotonic in it: take the lowest redraw over all of them
599+ let lower = kept . row < cut . row ? kept : cut ;
600+ for ( let b = loBottom + 1 ; b < hiBottom ; b ++ ) {
601+ const x = this . _reflowRows ( cols , b ) ;
602+ if ( x . row < lower . row ) { lower = x ; }
603+ }
604+ // The redraw begins where the lower of the two says the region begins. Where the cut end has the cursor lower, that is
605+ // below the first row of ITS region: the rows above it are not erased (they are what a copy of the prompt is) and
606+ // are not the rows it takes of the blank ones. (The kept end has the rows of the cut end and more, and a cursor
607+ // that is lower by no more than those rows: it is never the one with fewer.)
608+ const span = Math . max ( 0 , cut . rows . length - ( cut . row - lower . row ) ) ;
609+ return { row : lower . row , added : lower . added , removed : lower . removed , span, arranged : [ cut , kept ] } ;
590610 }
591611
592612 /**
593613 * `_cursorRowAfterReflow` for a given last row of the region that is still there.
594614 * @param {number } cols the new width
595615 * @param {number } bottom the last row of the region that the terminal still has (the rows under it were dropped)
596- * @returns {{ row: number, added: number, removed: number, rows: Array<{ line: number, cells: number }> } }
616+ * @returns {{ row: number, added: number, removed: number, rows: Array<{ line: number, cells: number }>, safe: number } }
597617 * the row, the rows the re-wrapping added and took out, and the screen's rows afterwards, top to bottom: the logical
598- * line each belongs to and the cells it holds
618+ * line each belongs to and the cells it holds; and how many of the first of them were at or above the cursor
599619 */
600620 _reflowRows ( cols , bottom ) {
601621 const top = this . winTop ;
@@ -640,7 +660,6 @@ class LineEditor {
640660 let removed = 0 ;
641661 if ( cols < this . cols ) {
642662 for ( let k = on . length - 1 ; k >= 0 ; k -- ) {
643- if ( was [ k ] === 1 && now [ k ] === 1 ) { continue ; } // not wrapped, and not too wide: not touched
644663 if ( holds ( row , k ) ) { continue ; }
645664 wrapped [ k ] = true ;
646665 const gain = Math . max ( 0 , now [ k ] - was [ k ] ) ;
@@ -650,7 +669,7 @@ class LineEditor {
650669 } else {
651670 const at = row ;
652671 for ( let k = 0 ; k < on . length ; k ++ ) {
653- if ( was [ k ] < 2 || holds ( at , k ) ) { continue ; } // only wrapped lines are joined
672+ if ( holds ( at , k ) ) { continue ; }
654673 wrapped [ k ] = true ;
655674 const loss = Math . max ( 0 , was [ k ] - now [ k ] ) ;
656675 row -= loss ;
@@ -660,21 +679,27 @@ class LineEditor {
660679 // the rows the screen has afterwards: a line that was not touched has its rows as they were, each cut at the new width
661680 /** @type {Array<{ line: number, cells: number }> } */
662681 const rows = [ ] ;
682+ // and how many of the first of them were at or above the cursor row: the lines above its line, and the rows of its
683+ // own line down to it (a cursor above the region, which a wider panel can leave it, has none)
684+ let safe = 0 ;
685+ const at0 = this . cursorRow ;
663686 on . forEach ( ( l , k ) => {
687+ const first = rows . length ;
664688 if ( wrapped [ k ] ) {
665689 for ( const n of packed [ k ] ) { rows . push ( { line : k , cells : n } ) ; }
666- return ;
667- }
668- for ( const widths of l . byRow ) {
669- let held = 0 ;
670- for ( const w of widths || [ ] ) {
671- if ( held + w > cols ) { break ; }
672- held += w ;
690+ } else {
691+ for ( const widths of l . byRow ) {
692+ let held = 0 ;
693+ for ( const w of widths || [ ] ) {
694+ if ( held + w > cols ) { break ; }
695+ held += w ;
696+ }
697+ rows . push ( { line : k , cells : held } ) ;
673698 }
674- rows . push ( { line : k , cells : held } ) ;
675699 }
700+ if ( start [ k ] + was [ k ] <= at0 ) { safe += rows . length - first ; } else if ( start [ k ] <= at0 ) { safe += Math . min ( rows . length - first , at0 - start [ k ] + 1 ) ; }
676701 } ) ;
677- return { row, added, removed, rows } ;
702+ return { row, added, removed, rows, safe } ;
678703 }
679704
680705 text ( ) { return this . buf . join ( '' ) ; }
0 commit comments