diff --git a/virtualizer/test/resize.test.ts b/virtualizer/test/resize.test.ts index 0521edb..187ca1f 100644 --- a/virtualizer/test/resize.test.ts +++ b/virtualizer/test/resize.test.ts @@ -70,6 +70,21 @@ describe("C.RESIZE — resize", () => { expect(v.anchorSubRow).toBe(1); }); + it("C.RESIZE.anchor-subrow-clamped-exact — uses exact wrapping for clamp, not estimate", () => { + let v = new Virtualizer({ measureWidth: charMeasure, columns: 3, rows: 24 }); + // "文" is width 2. At columns=3: 1 fits per row (2+2=4 > 3) → 10 exact sub-rows + // estimate = ceil(20/3) = 7 + v.appendLine("文".repeat(10)); + v.scrollBy(8); // anchorSubRow = 8 (valid in exact range 0-9) + expect(v.anchorSubRow).toBe(8); + + // Resize to columns=5: 2 chars fit per row (2+2=4 ≤ 5) → 5 exact sub-rows + // estimate = ceil(20/5) = 4 + // Should clamp to exact(5)-1 = 4, not estimate(4)-1 = 3 + v.resize(5, 24); + expect(v.anchorSubRow).toBe(4); + }); + it("C.RESIZE.bottom-follow-preserved — isAtBottom survives resize", () => { let v = new Virtualizer({ measureWidth: charMeasure, columns: 80, rows: 24 }); v.appendLine("hello"); diff --git a/virtualizer/test/scroll.test.ts b/virtualizer/test/scroll.test.ts index 79b8303..82c0ffa 100644 --- a/virtualizer/test/scroll.test.ts +++ b/virtualizer/test/scroll.test.ts @@ -115,4 +115,14 @@ describe("C.SCROLL — scrollBy", () => { prev = v.currentEstimatedVisualRow; } }); + + it("C.ESTIMATE.current-within-total — holds when exact wraps exceed estimate", () => { + let v = new Virtualizer({ measureWidth: charMeasure, columns: 3, rows: 24 }); + // "文" is width 2. At columns=3: 1 per row → 10 exact sub-rows + // estimate = ceil(20/3) = 7 + v.appendLine("文".repeat(10)); + v.scrollBy(9); // scroll to last exact sub-row + expect(v.currentEstimatedVisualRow).toBeGreaterThanOrEqual(0); + expect(v.currentEstimatedVisualRow).toBeLessThan(v.totalEstimatedVisualRows); + }); }); diff --git a/virtualizer/test/wrap-golden.test.ts b/virtualizer/test/wrap-golden.test.ts index a14af3a..23ece6e 100644 --- a/virtualizer/test/wrap-golden.test.ts +++ b/virtualizer/test/wrap-golden.test.ts @@ -81,4 +81,16 @@ describe("G.WRAP — wrapping golden fixtures", () => { expect(entry.wrapPoints).toEqual([]); expect(entry.totalSubRows).toBe(1); }); + + it("G.WRAP.wide-char-wider-than-columns — no wrap point at 0", () => { + let entry = resolve("文", 1); + expect(entry.wrapPoints).toEqual([]); + expect(entry.totalSubRows).toBe(1); + }); + + it("G.WRAP.multiple-wide-chars-at-columns-one — each on own row", () => { + let entry = resolve("文字", 1); + expect(entry.wrapPoints).toEqual([1]); + expect(entry.totalSubRows).toBe(2); + }); }); diff --git a/virtualizer/virtualizer.ts b/virtualizer/virtualizer.ts index ad9c258..d0be700 100644 --- a/virtualizer/virtualizer.ts +++ b/virtualizer/virtualizer.ts @@ -157,19 +157,20 @@ export class Virtualizer { } this._totalEstimatedVisualRows = newTotal; - // Clamp anchor sub-row at new width + this._columns = columns; + + // Clamp anchor sub-row using exact wrap count at new width if (this._ringBuffer.lineCount > 0) { let anchorEntry = this._ringBuffer.get(this._anchorLineIndex); if (anchorEntry) { - let newEstimate = Math.max(1, Math.ceil(anchorEntry.displayWidth / columns)); - if (this._anchorSubRow >= newEstimate) { - this._anchorSubRow = newEstimate - 1; + let wrapPoints = this._getWrapPoints(this._anchorLineIndex, anchorEntry.text); + let exactSubRows = wrapPoints.length + 1; + if (this._anchorSubRow >= exactSubRows) { + this._anchorSubRow = exactSubRows - 1; } } } - this._columns = columns; - // Recompute currentEstimatedVisualRow this._recomputeCurrentEstimate(); } @@ -318,7 +319,10 @@ export class Virtualizer { estimate += this._estimateVisualRows(entry.displayWidth); } estimate += this._anchorSubRow; - this._currentEstimatedVisualRow = estimate; + this._currentEstimatedVisualRow = Math.min( + estimate, + Math.max(this._totalEstimatedVisualRows - 1, 0), + ); } getLineDisplayWidth(lineIndex: number): number | undefined { diff --git a/virtualizer/wrap-walker.ts b/virtualizer/wrap-walker.ts index 0334cf0..79e30ce 100644 --- a/virtualizer/wrap-walker.ts +++ b/virtualizer/wrap-walker.ts @@ -53,7 +53,7 @@ export function computeWrapPoints( let charLen = cp > 0xffff ? 2 : 1; let w = measureWidth(String.fromCodePoint(cp)); - if (w > 0 && col + w > columns) { + if (col > 0 && col + w > columns) { wrapPoints.push(i); col = 0; }