@simon_he/vue-tui 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1008,6 +1008,15 @@ function normalizeInt$9(value) {
1008
1008
  const n = Math.floor(Number(value));
1009
1009
  return Number.isFinite(n) ? n : 0;
1010
1010
  }
1011
+ function normalizeChangedRange(range, count) {
1012
+ if (!range) return null;
1013
+ const start = clamp$10(normalizeInt$9(range.start), 0, count);
1014
+ const end = clamp$10(normalizeInt$9(range.end), start, count);
1015
+ return {
1016
+ start,
1017
+ end
1018
+ };
1019
+ }
1011
1020
  function getWheelScrollInput$1(e) {
1012
1021
  const deltaY = Number(e.deltaY ?? 0);
1013
1022
  const deltaMode = typeof e.deltaMode === "number" ? e.deltaMode : void 0;
@@ -1066,6 +1075,10 @@ const TVirtualRows = defineComponent({
1066
1075
  type: Number,
1067
1076
  required: true
1068
1077
  },
1078
+ itemChangedRange: {
1079
+ type: Function,
1080
+ default: void 0
1081
+ },
1069
1082
  getItem: {
1070
1083
  type: Function,
1071
1084
  required: true
@@ -1269,6 +1282,20 @@ const TVirtualRows = defineComponent({
1269
1282
  reason: "scroll"
1270
1283
  });
1271
1284
  }
1285
+ function markScrollDamage(prevTop, nextTop, strategy = "auto") {
1286
+ const r = normalizedRect();
1287
+ const h$1 = r.h;
1288
+ const delta = nextTop - prevTop;
1289
+ if (h$1 <= 0 || !delta) return;
1290
+ const size = terminal.size();
1291
+ const ownsFullRows = Math.floor(r.x) === 0 && Math.floor(r.w) >= size.cols;
1292
+ const withinTerminalRows = r.y >= 0 && r.y + h$1 <= size.rows;
1293
+ const canUseScrollPlane = strategy === "auto" && props.rowScrollMode === "unsafe-full-row" && rendererCapabilities.value.scrollOperations && ownsFullRows && withinTerminalRows && !isClipped() && Math.abs(delta) < h$1 && !dirtyRowsHint?.length;
1294
+ if (canUseScrollPlane) {
1295
+ render.unsafeScrollPlaneRows(plane.value, r.y, r.y + h$1, delta);
1296
+ markRowsDirty(exposedRowsForDelta$1(r.y, h$1, delta));
1297
+ } else markViewportDirty();
1298
+ }
1272
1299
  function applyScrollTop(nextTop, options) {
1273
1300
  const r = normalizedRect();
1274
1301
  const h$1 = r.h;
@@ -1284,14 +1311,7 @@ const TVirtualRows = defineComponent({
1284
1311
  }
1285
1312
  setCurrentScrollTop(clampedTop);
1286
1313
  if (options?.emitUpdate !== false) emit("update:scrollTop", clampedTop);
1287
- const size = terminal.size();
1288
- const ownsFullRows = Math.floor(r.x) === 0 && Math.floor(r.w) >= size.cols;
1289
- const withinTerminalRows = r.y >= 0 && r.y + h$1 <= size.rows;
1290
- const canUseScrollPlane = (options?.strategy ?? "auto") === "auto" && props.rowScrollMode === "unsafe-full-row" && rendererCapabilities.value.scrollOperations && ownsFullRows && withinTerminalRows && !isClipped() && Math.abs(delta) < h$1 && !dirtyRowsHint?.length;
1291
- if (canUseScrollPlane) {
1292
- render.unsafeScrollPlaneRows(plane.value, r.y, r.y + h$1, delta);
1293
- markRowsDirty(exposedRowsForDelta$1(r.y, h$1, delta));
1294
- } else markViewportDirty();
1314
+ markScrollDamage(prevTop, clampedTop, options?.strategy);
1295
1315
  if (options?.emitScroll) emitScroll(clampedTop);
1296
1316
  return true;
1297
1317
  }
@@ -1359,7 +1379,10 @@ const TVirtualRows = defineComponent({
1359
1379
  invalidateSelf("normal");
1360
1380
  }
1361
1381
  function invalidateRange(start, end) {
1362
- if (!hasPaintableViewport()) return;
1382
+ markItemRangeDirty(start, end);
1383
+ }
1384
+ function markItemRangeDirty(start, end) {
1385
+ if (!hasPaintableViewport()) return false;
1363
1386
  const r = normalizedRect();
1364
1387
  const { y: clipY } = clipOffsets();
1365
1388
  const top = currentScrollTop() + clipY;
@@ -1370,9 +1393,10 @@ const TVirtualRows = defineComponent({
1370
1393
  const y = r.y + (index - top);
1371
1394
  if (y >= r.y && y < r.y + r.h) rows.push(y);
1372
1395
  }
1373
- if (!rows.length) return;
1396
+ if (!rows.length) return true;
1374
1397
  markRowsDirty(rows);
1375
1398
  invalidateSelf("normal");
1399
+ return true;
1376
1400
  }
1377
1401
  function invalidateIndex(index) {
1378
1402
  const n = normalizeInt$9(index);
@@ -1581,8 +1605,9 @@ const TVirtualRows = defineComponent({
1581
1605
  return;
1582
1606
  }
1583
1607
  cancelWheelScrollFrame();
1608
+ const prevTop = controlledScrollTop.value;
1584
1609
  controlledScrollTop.value = nextTop;
1585
- markViewportDirty();
1610
+ markScrollDamage(prevTop, nextTop);
1586
1611
  selection.refresh(pendingSelectionScrollFocusRemap ? { remapFocus: true } : void 0);
1587
1612
  pendingSelectionScrollFocusRemap = false;
1588
1613
  invalidateSelf("high");
@@ -1602,7 +1627,7 @@ const TVirtualRows = defineComponent({
1602
1627
  () => absRect.value.y,
1603
1628
  () => absRect.value.w,
1604
1629
  () => absRect.value.h
1605
- ], () => {
1630
+ ], (next, prev) => {
1606
1631
  resetWheelScrollState(wheelState);
1607
1632
  const nextTop = clampScrollTop(currentScrollTop());
1608
1633
  if (isScrollControlled()) {
@@ -1611,6 +1636,14 @@ const TVirtualRows = defineComponent({
1611
1636
  emitScroll(nextTop);
1612
1637
  }
1613
1638
  } else setCurrentScrollTop(nextTop);
1639
+ const onlyItemVersionChanged = Boolean(prev && next[0] === prev[0] && next[1] !== prev[1] && next[2] === prev[2] && next[3] === prev[3] && next[4] === prev[4] && next[5] === prev[5] && next[6] === prev[6] && next[7] === prev[7] && next[8] === prev[8] && next[9] === prev[9]);
1640
+ if (onlyItemVersionChanged) {
1641
+ const changedRange = normalizeChangedRange(props.itemChangedRange?.(), itemCount.value);
1642
+ if (changedRange) {
1643
+ markItemRangeDirty(changedRange.start, changedRange.end);
1644
+ return;
1645
+ }
1646
+ }
1614
1647
  markViewportDirty();
1615
1648
  invalidateSelf("normal");
1616
1649
  }, { immediate: true });
@@ -1632,7 +1665,6 @@ const TVirtualRows = defineComponent({
1632
1665
  absRect.value,
1633
1666
  fullRect.value,
1634
1667
  itemCount.value,
1635
- props.itemVersion,
1636
1668
  props.getItem,
1637
1669
  props.paintItem,
1638
1670
  props.selectionSpanText,
@@ -1717,8 +1749,11 @@ const EMPTY_LAYOUT = Object.freeze({
1717
1749
  visualRows: Object.freeze([]),
1718
1750
  rowStarts: new Map(),
1719
1751
  rowEnds: new Map(),
1720
- regions: Object.freeze([])
1752
+ regions: Object.freeze([]),
1753
+ fixedRows: false,
1754
+ rowCount: 0
1721
1755
  });
1756
+ const MAX_KEYED_ROW_LAYOUT_CACHE_ENTRIES = 2048;
1722
1757
  function clamp$9(n, min, max) {
1723
1758
  return Math.max(min, Math.min(max, n));
1724
1759
  }
@@ -1726,6 +1761,15 @@ function normalizeInt$8(value) {
1726
1761
  const n = Math.floor(Number(value));
1727
1762
  return Number.isFinite(n) ? n : 0;
1728
1763
  }
1764
+ function normalizedChangedRange(range, count) {
1765
+ if (!range) return null;
1766
+ const start = clamp$9(normalizeInt$8(range.start), 0, count);
1767
+ const end = clamp$9(normalizeInt$8(range.end), start, count);
1768
+ return {
1769
+ start,
1770
+ end
1771
+ };
1772
+ }
1729
1773
  function sourceSegmentsForRegion(row) {
1730
1774
  if (row.kind === "message") return row.segments;
1731
1775
  if (row.kind === "approval") {
@@ -1756,8 +1800,26 @@ function rowHasRegionId(row, rowKey, id) {
1756
1800
  }
1757
1801
  return false;
1758
1802
  }
1759
- function sameCachedLayout(entry, next) {
1760
- return Boolean(entry) && (next.usesSourceRowVersion || entry.version === next.version) && entry.rowVersion === next.rowVersion && entry.usesSourceRowVersion === next.usesSourceRowVersion && (next.usesSourceRowVersion || entry.row === next.row) && entry.rowIndex === next.rowIndex && entry.rowKey === next.rowKey && entry.width === next.width && entry.wrap === next.wrap && entry.baseStyle === next.baseStyle && entry.hoverStyle === next.hoverStyle && entry.focusStyle === next.focusStyle && entry.hoverRegionId === next.hoverRegionId && entry.focusedRegionId === next.focusedRegionId;
1803
+ function cachedLayoutHasRegionId(entry, id) {
1804
+ if (!entry || !id) return false;
1805
+ for (const visualRow of entry.visualRows) if (visualRow.hitRegions.some((region) => region.id === id)) return true;
1806
+ return false;
1807
+ }
1808
+ function rowLayoutIdentityKey(rowKey, rowVersion) {
1809
+ return JSON.stringify([rowKey, rowVersion]);
1810
+ }
1811
+ function reindexVisualRows(rows, rowIndex) {
1812
+ return rows.map((visualRow) => ({
1813
+ ...visualRow,
1814
+ rowIndex,
1815
+ hitRegions: visualRow.hitRegions.map((region) => ({
1816
+ ...region,
1817
+ rowIndex
1818
+ }))
1819
+ }));
1820
+ }
1821
+ function sameCachedLayout(entry, next, matchRowIndex = true) {
1822
+ return Boolean(entry) && (next.usesSourceRowVersion || entry.version === next.version) && entry.rowVersion === next.rowVersion && entry.usesSourceRowVersion === next.usesSourceRowVersion && (next.usesSourceRowVersion || entry.row === next.row) && (!matchRowIndex || entry.rowIndex === next.rowIndex) && entry.rowKey === next.rowKey && entry.width === next.width && entry.wrap === next.wrap && entry.baseStyle === next.baseStyle && entry.hoverStyle === next.hoverStyle && entry.focusStyle === next.focusStyle && entry.hoverRegionId === next.hoverRegionId && entry.focusedRegionId === next.focusedRegionId;
1761
1823
  }
1762
1824
  const TTranscriptView = defineComponent({
1763
1825
  name: "TTranscriptView",
@@ -1861,6 +1923,10 @@ const TTranscriptView = defineComponent({
1861
1923
  const hoveredRegion = ref(null);
1862
1924
  const focusedRegion = ref(null);
1863
1925
  const rowLayoutCache = new Map();
1926
+ const keyedRowLayoutCache = new Map();
1927
+ let previousWrappedLayout = null;
1928
+ let previousWrappedDeps = null;
1929
+ let canUseWrappedChangedRangeRepaint = false;
1864
1930
  let warnedLargeFlattenedRows = false;
1865
1931
  function isScrollControlled() {
1866
1932
  return Object.prototype.hasOwnProperty.call(instance?.vnode.props ?? {}, "scrollTop");
@@ -1868,14 +1934,176 @@ const TTranscriptView = defineComponent({
1868
1934
  function currentScrollTop() {
1869
1935
  return normalizeInt$8(isScrollControlled() ? props.scrollTop : innerScrollTop.value);
1870
1936
  }
1937
+ function currentScrollTopForRowCount(count) {
1938
+ const viewportRows = Math.max(0, normalizeInt$8(props.h));
1939
+ const maxTop = Math.max(0, count - viewportRows);
1940
+ return clamp$9(currentScrollTop(), 0, maxTop);
1941
+ }
1942
+ function rememberKeyedRowLayout(entry) {
1943
+ if (!entry.usesSourceRowVersion) return;
1944
+ keyedRowLayoutCache.set(rowLayoutIdentityKey(entry.rowKey, entry.rowVersion), entry);
1945
+ if (keyedRowLayoutCache.size <= MAX_KEYED_ROW_LAYOUT_CACHE_ENTRIES) return;
1946
+ const first = keyedRowLayoutCache.keys().next().value;
1947
+ if (first != null) keyedRowLayoutCache.delete(first);
1948
+ }
1949
+ function layoutSourceRow(rowIndex, options) {
1950
+ const sourceRowVersion = props.source.getRowVersion?.(rowIndex);
1951
+ let cached = rowLayoutCache.get(rowIndex);
1952
+ const usesSourceRowVersion = sourceRowVersion != null;
1953
+ let row = cached?.row;
1954
+ let rowKey = cached?.rowKey;
1955
+ let canUseCachedRow = false;
1956
+ if (usesSourceRowVersion && cached?.usesSourceRowVersion && cached.rowVersion === sourceRowVersion) if (props.source.getRowKey) {
1957
+ rowKey = props.source.getRowKey(rowIndex);
1958
+ canUseCachedRow = rowKey === cached.rowKey;
1959
+ } else {
1960
+ rowKey = cached.rowKey;
1961
+ canUseCachedRow = true;
1962
+ }
1963
+ if (!canUseCachedRow && usesSourceRowVersion && props.source.getRowKey) {
1964
+ rowKey = props.source.getRowKey(rowIndex);
1965
+ cached = keyedRowLayoutCache.get(rowLayoutIdentityKey(rowKey, sourceRowVersion));
1966
+ if (cached) {
1967
+ row = cached.row;
1968
+ canUseCachedRow = true;
1969
+ }
1970
+ }
1971
+ if (!canUseCachedRow || row == null || rowKey == null) {
1972
+ row = props.source.getRow(rowIndex);
1973
+ rowKey = props.source.getRowKey?.(rowIndex) ?? row.key;
1974
+ canUseCachedRow = false;
1975
+ }
1976
+ const nextHoverRegionId = canUseCachedRow ? cachedLayoutHasRegionId(cached, options.hoverRegionId) ? options.hoverRegionId : null : rowHasRegionId(row, rowKey, options.hoverRegionId) ? options.hoverRegionId : null;
1977
+ const nextFocusedRegionId = canUseCachedRow ? cachedLayoutHasRegionId(cached, options.focusedRegionId) ? options.focusedRegionId : null : rowHasRegionId(row, rowKey, options.focusedRegionId) ? options.focusedRegionId : null;
1978
+ const cacheKey = {
1979
+ version: props.version,
1980
+ rowVersion: sourceRowVersion ?? props.version,
1981
+ usesSourceRowVersion,
1982
+ row,
1983
+ rowIndex,
1984
+ rowKey,
1985
+ width: options.width,
1986
+ wrap: props.wrap,
1987
+ baseStyle: options.baseStyle,
1988
+ hoverStyle: props.hoverStyle,
1989
+ focusStyle: props.focusStyle,
1990
+ hoverRegionId: nextHoverRegionId,
1991
+ focusedRegionId: nextFocusedRegionId
1992
+ };
1993
+ const cacheMatches = sameCachedLayout(cached, cacheKey);
1994
+ const reindexableCacheMatches = !cacheMatches && canUseCachedRow && sameCachedLayout(cached, cacheKey, false);
1995
+ const rows = cacheMatches ? cached.visualRows : reindexableCacheMatches ? reindexVisualRows(cached.visualRows, rowIndex) : layoutTranscriptRow({
1996
+ row,
1997
+ rowIndex,
1998
+ rowKey,
1999
+ width: options.width,
2000
+ baseStyle: options.baseStyle,
2001
+ hoverRegionId: nextHoverRegionId,
2002
+ focusedRegionId: nextFocusedRegionId,
2003
+ hoverStyle: props.hoverStyle,
2004
+ focusStyle: props.focusStyle,
2005
+ wrap: props.wrap
2006
+ });
2007
+ if (rows !== cached?.visualRows) {
2008
+ const entry = {
2009
+ ...cacheKey,
2010
+ visualRows: rows
2011
+ };
2012
+ rowLayoutCache.set(rowIndex, entry);
2013
+ rememberKeyedRowLayout(entry);
2014
+ }
2015
+ return {
2016
+ rows,
2017
+ cacheHit: cacheMatches || reindexableCacheMatches
2018
+ };
2019
+ }
2020
+ function pruneRowLayoutCache(count) {
2021
+ for (const rowIndex of rowLayoutCache.keys()) if (rowIndex >= count) rowLayoutCache.delete(rowIndex);
2022
+ }
2023
+ function sameWrappedDeps(a, b) {
2024
+ return Boolean(a) && a.width === b.width && a.baseStyle === b.baseStyle && a.hoverStyle === b.hoverStyle && a.focusStyle === b.focusStyle && a.hoverRegionId === b.hoverRegionId && a.focusedRegionId === b.focusedRegionId;
2025
+ }
2026
+ function visibleRegionsForVisualRows(visualRows) {
2027
+ const top = clamp$9(currentScrollTop(), 0, visualRows.length);
2028
+ const bottom = Math.min(visualRows.length, top + Math.max(0, normalizeInt$8(props.h)));
2029
+ const regions = [];
2030
+ for (let index = top; index < bottom; index++) regions.push(...visualRows[index]?.hitRegions ?? []);
2031
+ return regions;
2032
+ }
2033
+ function tryUpdateWrappedLayoutFromChangedRange(count, changedRange, deps) {
2034
+ const previous = previousWrappedLayout;
2035
+ if (!previous || previous.fixedRows || previous.rowCount !== count) return null;
2036
+ if (!changedRange || !sameWrappedDeps(previousWrappedDeps, deps)) return null;
2037
+ const changedCount = changedRange.end - changedRange.start;
2038
+ if (changedCount <= 0) {
2039
+ canUseWrappedChangedRangeRepaint = true;
2040
+ return {
2041
+ state: {
2042
+ ...previous,
2043
+ regions: visibleRegionsForVisualRows(previous.visualRows)
2044
+ },
2045
+ cacheHit: count,
2046
+ cacheMiss: 0
2047
+ };
2048
+ }
2049
+ const oldVisualStart = previous.rowStarts.get(changedRange.start);
2050
+ const oldVisualEnd = previous.rowEnds.get(changedRange.end - 1);
2051
+ if (oldVisualStart == null || oldVisualEnd == null) return null;
2052
+ const nextRows = [];
2053
+ const changedRowHeights = [];
2054
+ let cacheHit = count - changedCount;
2055
+ let cacheMiss = 0;
2056
+ for (let rowIndex = changedRange.start; rowIndex < changedRange.end; rowIndex++) {
2057
+ const result = layoutSourceRow(rowIndex, {
2058
+ width: deps.width,
2059
+ baseStyle: deps.baseStyle,
2060
+ hoverRegionId: deps.hoverRegionId,
2061
+ focusedRegionId: deps.focusedRegionId
2062
+ });
2063
+ if (result.cacheHit) cacheHit++;
2064
+ else cacheMiss++;
2065
+ changedRowHeights.push(result.rows.length);
2066
+ nextRows.push(...result.rows);
2067
+ }
2068
+ if (nextRows.length !== oldVisualEnd - oldVisualStart) return null;
2069
+ canUseWrappedChangedRangeRepaint = true;
2070
+ const visualRows = previous.visualRows;
2071
+ for (let index = 0; index < nextRows.length; index++) visualRows[oldVisualStart + index] = nextRows[index];
2072
+ const rowStarts = previous.rowStarts;
2073
+ const rowEnds = previous.rowEnds;
2074
+ let visualCursor = oldVisualStart;
2075
+ for (let rowIndex = changedRange.start, offset = 0; rowIndex < changedRange.end; rowIndex++, offset++) {
2076
+ rowStarts.set(rowIndex, visualCursor);
2077
+ visualCursor += changedRowHeights[offset] ?? 0;
2078
+ rowEnds.set(rowIndex, visualCursor);
2079
+ }
2080
+ const state = {
2081
+ visualRows,
2082
+ rowStarts,
2083
+ rowEnds,
2084
+ regions: visibleRegionsForVisualRows(visualRows),
2085
+ fixedRows: false,
2086
+ rowCount: count
2087
+ };
2088
+ previousWrappedLayout = state;
2089
+ return {
2090
+ state,
2091
+ cacheHit,
2092
+ cacheMiss
2093
+ };
2094
+ }
1871
2095
  const layoutState = computed(() => {
1872
2096
  props.version;
2097
+ canUseWrappedChangedRangeRepaint = false;
1873
2098
  const perfEnabled = observability.framePerf.enabled.value;
1874
2099
  const perfStartedAt = perfEnabled ? framePerfNow() : 0;
1875
2100
  const width = Math.max(1, Math.floor(props.w));
1876
2101
  const count = Math.max(0, normalizeInt$8(props.source.rowCount()));
1877
2102
  if (!count) {
1878
2103
  rowLayoutCache.clear();
2104
+ keyedRowLayoutCache.clear();
2105
+ previousWrappedLayout = null;
2106
+ previousWrappedDeps = null;
1879
2107
  if (perfEnabled) observability.framePerf.recordComponent({
1880
2108
  name: "TTranscriptView",
1881
2109
  id: instance?.uid == null ? void 0 : String(instance.uid),
@@ -1890,7 +2118,9 @@ const TTranscriptView = defineComponent({
1890
2118
  });
1891
2119
  return EMPTY_LAYOUT;
1892
2120
  }
2121
+ const fixedRows = props.wrap === false;
1893
2122
  const visualRows = [];
2123
+ if (fixedRows) visualRows.length = count;
1894
2124
  const rowStarts = new Map();
1895
2125
  const rowEnds = new Map();
1896
2126
  const regions = [];
@@ -1899,56 +2129,71 @@ const TTranscriptView = defineComponent({
1899
2129
  const focusedRegionId = focusedRegion.value?.id ?? null;
1900
2130
  let cacheHit = 0;
1901
2131
  let cacheMiss = 0;
1902
- for (let rowIndex = 0; rowIndex < count; rowIndex++) {
1903
- const row = props.source.getRow(rowIndex);
1904
- const rowKey = props.source.getRowKey?.(rowIndex) ?? row.key;
1905
- const sourceRowVersion = props.source.getRowVersion?.(rowIndex);
1906
- const localHoverRegionId = rowHasRegionId(row, rowKey, hoverRegionId) ? hoverRegionId : null;
1907
- const localFocusedRegionId = rowHasRegionId(row, rowKey, focusedRegionId) ? focusedRegionId : null;
1908
- const cacheKey = {
1909
- version: props.version,
1910
- rowVersion: sourceRowVersion ?? props.version,
1911
- usesSourceRowVersion: sourceRowVersion != null,
1912
- row,
1913
- rowIndex,
1914
- rowKey,
1915
- width,
1916
- wrap: props.wrap,
1917
- baseStyle,
1918
- hoverStyle: props.hoverStyle,
1919
- focusStyle: props.focusStyle,
1920
- hoverRegionId: localHoverRegionId,
1921
- focusedRegionId: localFocusedRegionId
1922
- };
1923
- rowStarts.set(rowIndex, visualRows.length);
1924
- const cached = rowLayoutCache.get(rowIndex);
1925
- const cacheMatches = sameCachedLayout(cached, cacheKey);
1926
- if (cacheMatches) cacheHit++;
1927
- else cacheMiss++;
1928
- const rows = cacheMatches ? cached.visualRows : layoutTranscriptRow({
1929
- row,
1930
- rowIndex,
1931
- rowKey,
1932
- width,
1933
- baseStyle,
1934
- hoverRegionId: localHoverRegionId,
1935
- focusedRegionId: localFocusedRegionId,
1936
- hoverStyle: props.hoverStyle,
1937
- focusStyle: props.focusStyle,
1938
- wrap: props.wrap
1939
- });
1940
- if (rows !== cached?.visualRows) rowLayoutCache.set(rowIndex, {
1941
- ...cacheKey,
1942
- visualRows: rows
1943
- });
1944
- for (const visualRow of rows) {
1945
- visualRows.push(visualRow);
1946
- regions.push(...visualRow.hitRegions);
2132
+ const sourceChangedRange = normalizedChangedRange(props.source.getChangedRange?.(), count);
2133
+ const wrappedDeps = {
2134
+ width,
2135
+ baseStyle,
2136
+ hoverStyle: props.hoverStyle,
2137
+ focusStyle: props.focusStyle,
2138
+ hoverRegionId,
2139
+ focusedRegionId
2140
+ };
2141
+ if (fixedRows) {
2142
+ previousWrappedLayout = null;
2143
+ previousWrappedDeps = null;
2144
+ const top = currentScrollTopForRowCount(count);
2145
+ const bottom = Math.min(count, top + Math.max(0, normalizeInt$8(props.h)));
2146
+ for (let rowIndex = top; rowIndex < bottom; rowIndex++) {
2147
+ rowStarts.set(rowIndex, rowIndex);
2148
+ rowEnds.set(rowIndex, rowIndex + 1);
2149
+ const result = layoutSourceRow(rowIndex, {
2150
+ width,
2151
+ baseStyle,
2152
+ hoverRegionId,
2153
+ focusedRegionId
2154
+ });
2155
+ if (result.cacheHit) cacheHit++;
2156
+ else cacheMiss++;
2157
+ const row = result.rows[0];
2158
+ visualRows[rowIndex] = row;
2159
+ regions.push(...row.hitRegions);
2160
+ }
2161
+ } else {
2162
+ const incremental = tryUpdateWrappedLayoutFromChangedRange(count, sourceChangedRange, wrappedDeps);
2163
+ if (incremental) {
2164
+ if (perfEnabled) observability.framePerf.recordComponent({
2165
+ name: "TTranscriptView",
2166
+ id: instance?.uid == null ? void 0 : String(instance.uid),
2167
+ phase: "layout",
2168
+ durationMs: framePerfNow() - perfStartedAt,
2169
+ itemCount: count,
2170
+ renderedCount: incremental.state.visualRows.length,
2171
+ cacheHit: incremental.cacheHit,
2172
+ cacheMiss: incremental.cacheMiss,
2173
+ width,
2174
+ version: props.version
2175
+ });
2176
+ return incremental.state;
2177
+ }
2178
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
2179
+ rowStarts.set(rowIndex, visualRows.length);
2180
+ const result = layoutSourceRow(rowIndex, {
2181
+ width,
2182
+ baseStyle,
2183
+ hoverRegionId,
2184
+ focusedRegionId
2185
+ });
2186
+ if (result.cacheHit) cacheHit++;
2187
+ else cacheMiss++;
2188
+ for (const visualRow of result.rows) {
2189
+ visualRows.push(visualRow);
2190
+ regions.push(...visualRow.hitRegions);
2191
+ }
2192
+ rowEnds.set(rowIndex, visualRows.length);
1947
2193
  }
1948
- rowEnds.set(rowIndex, visualRows.length);
1949
2194
  }
1950
- for (const rowIndex of rowLayoutCache.keys()) if (rowIndex >= count) rowLayoutCache.delete(rowIndex);
1951
- if (!warnedLargeFlattenedRows && globalThis.__VT_DEBUG_PERF__ && visualRows.length > 5e3) {
2195
+ pruneRowLayoutCache(count);
2196
+ if (!warnedLargeFlattenedRows && !fixedRows && globalThis.__VT_DEBUG_PERF__ && visualRows.length > 5e3) {
1952
2197
  warnedLargeFlattenedRows = true;
1953
2198
  console.warn("[vue-tui] TTranscriptView flattens all transcript rows; use TLogView or windowed source for large retained output.");
1954
2199
  }
@@ -1958,18 +2203,28 @@ const TTranscriptView = defineComponent({
1958
2203
  phase: "layout",
1959
2204
  durationMs: framePerfNow() - perfStartedAt,
1960
2205
  itemCount: count,
1961
- renderedCount: visualRows.length,
2206
+ renderedCount: fixedRows ? cacheHit + cacheMiss : visualRows.length,
1962
2207
  cacheHit,
1963
2208
  cacheMiss,
1964
2209
  width,
1965
2210
  version: props.version
1966
2211
  });
1967
- return {
2212
+ const state = {
1968
2213
  visualRows,
1969
2214
  rowStarts,
1970
2215
  rowEnds,
1971
- regions
2216
+ regions,
2217
+ fixedRows,
2218
+ rowCount: count
1972
2219
  };
2220
+ if (fixedRows) {
2221
+ previousWrappedLayout = null;
2222
+ previousWrappedDeps = null;
2223
+ } else {
2224
+ previousWrappedLayout = state;
2225
+ previousWrappedDeps = wrappedDeps;
2226
+ }
2227
+ return state;
1973
2228
  });
1974
2229
  const maxScrollTop = computed(() => Math.max(0, layoutState.value.visualRows.length - Math.max(0, normalizeInt$8(props.h))));
1975
2230
  function setScrollTop(top) {
@@ -2007,7 +2262,12 @@ const TTranscriptView = defineComponent({
2007
2262
  if (!region) return [];
2008
2263
  const rows = layoutState.value.visualRows;
2009
2264
  const indexes = [];
2010
- for (let i = 0; i < rows.length; i++) if (rows[i].hitRegions.some((candidate) => candidate.id === region.id)) indexes.push(i);
2265
+ const top = layoutState.value.fixedRows ? clamp$9(currentScrollTop(), 0, rows.length) : 0;
2266
+ const bottom = layoutState.value.fixedRows ? Math.min(rows.length, top + Math.max(0, normalizeInt$8(props.h))) : rows.length;
2267
+ for (let i = top; i < bottom; i++) {
2268
+ const row = getVisualRow(i);
2269
+ if (row?.hitRegions.some((candidate) => candidate.id === region.id)) indexes.push(i);
2270
+ }
2011
2271
  return indexes;
2012
2272
  }
2013
2273
  function invalidateRegion(region) {
@@ -2034,7 +2294,7 @@ const TTranscriptView = defineComponent({
2034
2294
  const bottom = Math.min(rows.length, top + Math.max(0, normalizeInt$8(props.h)));
2035
2295
  const seen = new Set();
2036
2296
  const regions = [];
2037
- for (let i = top; i < bottom; i++) for (const region of rows[i]?.hitRegions ?? []) {
2297
+ for (let i = top; i < bottom; i++) for (const region of getVisualRow(i)?.hitRegions ?? []) {
2038
2298
  if (seen.has(region.id)) continue;
2039
2299
  seen.add(region.id);
2040
2300
  regions.push(region);
@@ -2045,7 +2305,7 @@ const TTranscriptView = defineComponent({
2045
2305
  const rows = layoutState.value.visualRows;
2046
2306
  const top = clamp$9(currentScrollTop(), 0, rows.length);
2047
2307
  const bottom = Math.min(rows.length, top + Math.max(0, normalizeInt$8(props.h)));
2048
- for (let i = top; i < bottom; i++) if (rows[i]?.hitRegions.some((region) => region.id === id)) return true;
2308
+ for (let i = top; i < bottom; i++) if (getVisualRow(i)?.hitRegions.some((region) => region.id === id)) return true;
2049
2309
  return false;
2050
2310
  }
2051
2311
  function reconcileActiveRegions() {
@@ -2102,8 +2362,8 @@ const TTranscriptView = defineComponent({
2102
2362
  }
2103
2363
  function scrollToRow(index, options) {
2104
2364
  const rowIndex = clamp$9(normalizeInt$8(index), 0, Math.max(0, props.source.rowCount() - 1));
2105
- const visualIndex = layoutState.value.rowStarts.get(rowIndex) ?? 0;
2106
- const rowEnd = layoutState.value.rowEnds.get(rowIndex) ?? visualIndex + 1;
2365
+ const visualIndex = layoutState.value.fixedRows ? rowIndex : layoutState.value.rowStarts.get(rowIndex) ?? 0;
2366
+ const rowEnd = layoutState.value.fixedRows ? rowIndex + 1 : layoutState.value.rowEnds.get(rowIndex) ?? visualIndex + 1;
2107
2367
  const viewportRows = Math.max(1, normalizeInt$8(props.h));
2108
2368
  const rowHeight = Math.max(1, rowEnd - visualIndex);
2109
2369
  let top = visualIndex;
@@ -2114,19 +2374,33 @@ const TTranscriptView = defineComponent({
2114
2374
  setScrollTop(top);
2115
2375
  }
2116
2376
  function invalidateRow(index) {
2117
- const start = layoutState.value.rowStarts.get(index);
2118
- const end = layoutState.value.rowEnds.get(index);
2377
+ const start = layoutState.value.fixedRows ? index : layoutState.value.rowStarts.get(index);
2378
+ const end = layoutState.value.fixedRows ? index + 1 : layoutState.value.rowEnds.get(index);
2119
2379
  if (start == null || end == null) return;
2120
2380
  rowsRef.value?.invalidateRange(start, end);
2121
2381
  }
2122
2382
  function invalidateRange(start, end) {
2123
2383
  const first = clamp$9(normalizeInt$8(start), 0, props.source.rowCount());
2124
2384
  const last = clamp$9(normalizeInt$8(end), first, props.source.rowCount());
2125
- const visualStart = layoutState.value.rowStarts.get(first);
2126
- const visualEnd = layoutState.value.rowEnds.get(last - 1);
2385
+ const visualStart = layoutState.value.fixedRows ? first : layoutState.value.rowStarts.get(first);
2386
+ const visualEnd = layoutState.value.fixedRows ? last : layoutState.value.rowEnds.get(last - 1);
2127
2387
  if (visualStart == null || visualEnd == null) return;
2128
2388
  rowsRef.value?.invalidateRange(visualStart, visualEnd);
2129
2389
  }
2390
+ function virtualRowsChangedRange() {
2391
+ const state = layoutState.value;
2392
+ const changedRange = normalizedChangedRange(props.source.getChangedRange?.(), state.rowCount);
2393
+ if (!changedRange) return null;
2394
+ if (state.fixedRows || changedRange.end <= changedRange.start) return changedRange;
2395
+ if (!canUseWrappedChangedRangeRepaint) return null;
2396
+ const visualStart = state.rowStarts.get(changedRange.start);
2397
+ const visualEnd = state.rowEnds.get(changedRange.end - 1);
2398
+ if (visualStart == null || visualEnd == null) return null;
2399
+ return {
2400
+ start: visualStart,
2401
+ end: visualEnd
2402
+ };
2403
+ }
2130
2404
  expose({
2131
2405
  scrollToBottom,
2132
2406
  scrollToTop,
@@ -2209,7 +2483,19 @@ const TTranscriptView = defineComponent({
2209
2483
  return text;
2210
2484
  }
2211
2485
  function getVisualRow(index) {
2212
- return layoutState.value.visualRows[index];
2486
+ const state = layoutState.value;
2487
+ const cached = state.visualRows[index];
2488
+ if (cached || !state.fixedRows) return cached;
2489
+ if (index < 0 || index >= state.visualRows.length) return void 0;
2490
+ const result = layoutSourceRow(index, {
2491
+ width: Math.max(1, Math.floor(props.w)),
2492
+ baseStyle: props.style ?? defaultStyle.value,
2493
+ hoverRegionId: hoveredRegion.value?.id ?? null,
2494
+ focusedRegionId: focusedRegion.value?.id ?? null
2495
+ });
2496
+ const row = result.rows[0];
2497
+ if (row) state.visualRows[index] = row;
2498
+ return row;
2213
2499
  }
2214
2500
  function selectionTextForVisualRow(visualRow) {
2215
2501
  if (!visualRow) return "";
@@ -2234,6 +2520,7 @@ const TTranscriptView = defineComponent({
2234
2520
  zIndex: props.zIndex,
2235
2521
  itemCount: layoutState.value.visualRows.length,
2236
2522
  itemVersion: props.version,
2523
+ itemChangedRange: virtualRowsChangedRange,
2237
2524
  getItem: getVisualRow,
2238
2525
  paintItem: paintVisualRow,
2239
2526
  renderItemNodes: renderVisualRowNodes,
@@ -2632,12 +2919,128 @@ function wrapStyledSegmentsByCells(segments, width) {
2632
2919
  });
2633
2920
  rowCells += cells;
2634
2921
  };
2635
- for (const seg of segments) forEachTextCellSegment(seg.text, (piece) => {
2922
+ for (const seg of segments) {
2923
+ const cells = textCellWidth(seg.text);
2924
+ if (cells === seg.text.length) {
2925
+ for (let i = 0; i < seg.text.length;) {
2926
+ if (rowCells >= width) openRow();
2927
+ const take = Math.min(width - rowCells, seg.text.length - i);
2928
+ if (take <= 0) break;
2929
+ row.push({
2930
+ text: seg.text.slice(i, i + take),
2931
+ cells: take,
2932
+ style: seg.style
2933
+ });
2934
+ rowCells += take;
2935
+ i += take;
2936
+ }
2937
+ continue;
2938
+ }
2939
+ forEachTextCellSegment(seg.text, (piece) => {
2940
+ if (!piece.text || piece.cells <= 0) return;
2941
+ pushWrappedPiece(piece.text, piece.cells, seg.style);
2942
+ });
2943
+ }
2944
+ return rows;
2945
+ }
2946
+ function countWrappedTextRows(text, width) {
2947
+ width = Math.max(1, Math.floor(width));
2948
+ if (!text) return 1;
2949
+ const cells = textCellWidth(text);
2950
+ if (cells <= 0) return 1;
2951
+ if (cells === text.length) return Math.max(1, Math.ceil(cells / width));
2952
+ let rows = 1;
2953
+ let rowCells = 0;
2954
+ forEachTextCellSegment(text, (piece) => {
2636
2955
  if (!piece.text || piece.cells <= 0) return;
2637
- pushWrappedPiece(piece.text, piece.cells, seg.style);
2956
+ const pieceCells = piece.cells;
2957
+ if (rowCells > 0 && rowCells + pieceCells > width) {
2958
+ rows++;
2959
+ rowCells = 0;
2960
+ }
2961
+ if (pieceCells > width) {
2962
+ let remaining = pieceCells;
2963
+ while (remaining > 0) {
2964
+ if (rowCells >= width) {
2965
+ rows++;
2966
+ rowCells = 0;
2967
+ }
2968
+ const take = Math.min(width - rowCells, remaining);
2969
+ rowCells += take;
2970
+ remaining -= take;
2971
+ }
2972
+ return;
2973
+ }
2974
+ if (rowCells >= width) {
2975
+ rows++;
2976
+ rowCells = 0;
2977
+ }
2978
+ rowCells += pieceCells;
2638
2979
  });
2639
2980
  return rows;
2640
2981
  }
2982
+ function countWrappedPlainTextRows(text, width) {
2983
+ width = Math.max(1, Math.floor(width));
2984
+ if (!text) return 1;
2985
+ let rows = 0;
2986
+ let rowCells = 0;
2987
+ let sawText = false;
2988
+ forEachTextCellSegment(text, (piece) => {
2989
+ if (!piece.text || piece.cells <= 0) return;
2990
+ sawText = true;
2991
+ const cells = piece.cells;
2992
+ if (rowCells > 0 && rowCells + cells > width) {
2993
+ rows++;
2994
+ rowCells = 0;
2995
+ }
2996
+ rowCells += cells;
2997
+ if (rowCells >= width) {
2998
+ rows++;
2999
+ rowCells = 0;
3000
+ }
3001
+ });
3002
+ return Math.max(1, rows + (rowCells > 0 || !sawText ? 1 : 0));
3003
+ }
3004
+ function countAsciiAnsiTextCells(text) {
3005
+ let cells = 0;
3006
+ for (let i = 0; i < text.length; i++) {
3007
+ const code = text.charCodeAt(i);
3008
+ if (code > 127) return null;
3009
+ if (code === 27) {
3010
+ const next = text[i + 1];
3011
+ if (next === "[") {
3012
+ let j = i + 2;
3013
+ while (j < text.length) {
3014
+ const c = text.charCodeAt(j);
3015
+ if (c >= 64 && c <= 126) break;
3016
+ j++;
3017
+ }
3018
+ if (j >= text.length) return null;
3019
+ i = j;
3020
+ continue;
3021
+ }
3022
+ if (next === "]") {
3023
+ let j = i + 2;
3024
+ while (j < text.length) {
3025
+ const c = text.charCodeAt(j);
3026
+ if (c === 7) break;
3027
+ if (c === 27 && text[j + 1] === "\\") {
3028
+ j++;
3029
+ break;
3030
+ }
3031
+ j++;
3032
+ }
3033
+ if (j >= text.length) return null;
3034
+ i = j;
3035
+ continue;
3036
+ }
3037
+ return null;
3038
+ }
3039
+ if (code <= 31 || code === 127) return null;
3040
+ cells++;
3041
+ }
3042
+ return cells;
3043
+ }
2641
3044
  const TLogView = defineComponent({
2642
3045
  name: "TLogView",
2643
3046
  props: {
@@ -2806,6 +3209,7 @@ const TLogView = defineComponent({
2806
3209
  const renderLineCache = new Map();
2807
3210
  const wrapLineCache = new Map();
2808
3211
  const ansiLineCache = new Map();
3212
+ const ansiTextCache = new Map();
2809
3213
  const ansiWrapCache = new Map();
2810
3214
  const ansiRowCache = new Map();
2811
3215
  const linkifyLineCache = new Map();
@@ -2946,6 +3350,9 @@ const TLogView = defineComponent({
2946
3350
  linkKey
2947
3351
  ]);
2948
3352
  }
3353
+ function ansiTextCacheKey(key) {
3354
+ return JSON.stringify(["ansi-text", key]);
3355
+ }
2949
3356
  function linkifyLineCacheKey(key, baseStyleKey, linkKey, optionsKey) {
2950
3357
  return JSON.stringify([
2951
3358
  "linkify-line",
@@ -3014,6 +3421,12 @@ const TLogView = defineComponent({
3014
3421
  const entries = Array.from(ansiLineCache.values()).sort((a, b) => a.touchedAt - b.touchedAt);
3015
3422
  for (const entry of entries.slice(0, ansiLineCache.size - max)) ansiLineCache.delete(entry.key);
3016
3423
  }
3424
+ function trimAnsiTextCache() {
3425
+ const max = DEFAULT_LOG_RENDER_CACHE_SIZE;
3426
+ if (ansiTextCache.size <= max) return;
3427
+ const entries = Array.from(ansiTextCache.values()).sort((a, b) => a.touchedAt - b.touchedAt);
3428
+ for (const entry of entries.slice(0, ansiTextCache.size - max)) ansiTextCache.delete(entry.key);
3429
+ }
3017
3430
  function trimAnsiWrapCache() {
3018
3431
  const max = DEFAULT_LOG_WRAP_CACHE_SIZE;
3019
3432
  if (ansiWrapCache.size <= max) return;
@@ -3048,6 +3461,7 @@ const TLogView = defineComponent({
3048
3461
  renderLineCache.clear();
3049
3462
  wrapLineCache.clear();
3050
3463
  ansiLineCache.clear();
3464
+ ansiTextCache.clear();
3051
3465
  ansiWrapCache.clear();
3052
3466
  ansiRowCache.clear();
3053
3467
  linkifyLineCache.clear();
@@ -3125,6 +3539,23 @@ const TLogView = defineComponent({
3125
3539
  });
3126
3540
  return segments;
3127
3541
  }
3542
+ function ansiPlainTextForLine(index, count) {
3543
+ if (index < 0 || index >= count) return "";
3544
+ const rawKey = lineKey(index);
3545
+ const key = ansiTextCacheKey(rawKey);
3546
+ const cached = ansiTextCache.get(key);
3547
+ if (cached) {
3548
+ cached.touchedAt = ++cacheClock;
3549
+ return cached.value;
3550
+ }
3551
+ const text = sanitizeAnsiInlineText(props.source.getLine(index));
3552
+ ansiTextCache.set(key, {
3553
+ key,
3554
+ value: text,
3555
+ touchedAt: ++cacheClock
3556
+ });
3557
+ return text;
3558
+ }
3128
3559
  function ansiWrappedRowsForLine(index, count, width, baseStyle, baseStyleKey, linkKey = linkStyleCacheKey()) {
3129
3560
  if (index < 0 || index >= count) return [[]];
3130
3561
  width = Math.max(1, Math.floor(width));
@@ -3481,8 +3912,8 @@ const TLogView = defineComponent({
3481
3912
  lineMatches.push(matchIndex);
3482
3913
  invalidateSearchMarkers();
3483
3914
  }
3484
- function searchableTextForLine(index, count, baseStyle, baseStyleKey) {
3485
- if (props.ansi) return ansiSegmentsForLine(index, count, baseStyle, baseStyleKey).map((seg) => seg.text).join("");
3915
+ function searchableTextForLine(index, count) {
3916
+ if (props.ansi) return ansiPlainTextForLine(index, count);
3486
3917
  return sanitizeInlineText(props.source.getLine(index));
3487
3918
  }
3488
3919
  function normalizeSearchResultPreviewWidth(value) {
@@ -3495,8 +3926,8 @@ const TLogView = defineComponent({
3495
3926
  if (!Number.isFinite(n)) return DEFAULT_SEARCH_RESULTS_CONTEXT_CELLS;
3496
3927
  return Math.max(0, n);
3497
3928
  }
3498
- function previewForMatch(match, count, baseStyle, baseStyleKey, options) {
3499
- const text = searchableTextForLine(match.index, count, baseStyle, baseStyleKey);
3929
+ function previewForMatch(match, count, options) {
3930
+ const text = searchableTextForLine(match.index, count);
3500
3931
  const totalCells = textCellWidth(text);
3501
3932
  const matchCells = Math.max(1, match.endCell - match.startCell);
3502
3933
  const previewWidth = Math.max(options.previewWidth, matchCells);
@@ -3526,7 +3957,7 @@ const TLogView = defineComponent({
3526
3957
  matchEndCell
3527
3958
  };
3528
3959
  }
3529
- function cachedLineSearchMatches(index, count, search, baseStyle, baseStyleKey) {
3960
+ function cachedLineSearchMatches(index, count, search) {
3530
3961
  const rawKey = lineKey(index);
3531
3962
  const key = searchLineCacheKey(rawKey, search.key);
3532
3963
  const cached = searchLineCache.get(key);
@@ -3534,7 +3965,7 @@ const TLogView = defineComponent({
3534
3965
  cached.touchedAt = ++cacheClock;
3535
3966
  return cached.matches;
3536
3967
  }
3537
- const matches = search.findLineMatches(searchableTextForLine(index, count, baseStyle, baseStyleKey));
3968
+ const matches = search.findLineMatches(searchableTextForLine(index, count));
3538
3969
  searchLineCache.set(key, {
3539
3970
  key,
3540
3971
  matches,
@@ -3542,9 +3973,9 @@ const TLogView = defineComponent({
3542
3973
  });
3543
3974
  return matches;
3544
3975
  }
3545
- function scanSearchLine(index, count, search, baseStyle, baseStyleKey, maxMatches, absoluteBase) {
3976
+ function scanSearchLine(index, count, search, maxMatches, absoluteBase) {
3546
3977
  if (searchMatches.length >= maxMatches) return;
3547
- const lineMatches = cachedLineSearchMatches(index, count, search, baseStyle, baseStyleKey);
3978
+ const lineMatches = cachedLineSearchMatches(index, count, search);
3548
3979
  for (const match of lineMatches) {
3549
3980
  if (searchMatches.length >= maxMatches) return;
3550
3981
  addSearchMatch({
@@ -3572,11 +4003,9 @@ const TLogView = defineComponent({
3572
4003
  const count = lineCount();
3573
4004
  const maxMatches = maxSearchMatches();
3574
4005
  const absoluteBase = firstLineIndex();
3575
- const base = props.style ?? defaultStyle.value;
3576
- const baseStyleKey = styleCacheKey(base);
3577
4006
  let scanned = 0;
3578
4007
  while (searchCursor < count && searchMatches.length < maxMatches && (scanned === 0 || ctx.now() - started < budget)) {
3579
- scanSearchLine(searchCursor, count, compiledSearch, base, baseStyleKey, maxMatches, absoluteBase);
4008
+ scanSearchLine(searchCursor, count, compiledSearch, maxMatches, absoluteBase);
3580
4009
  searchCursor++;
3581
4010
  scanned++;
3582
4011
  }
@@ -3601,6 +4030,7 @@ const TLogView = defineComponent({
3601
4030
  plane: plane.value,
3602
4031
  reason: "data"
3603
4032
  });
4033
+ trimAnsiTextCache();
3604
4034
  trimSearchLineCache();
3605
4035
  }
3606
4036
  function requestSearchScan() {
@@ -3846,14 +4276,22 @@ const TLogView = defineComponent({
3846
4276
  }
3847
4277
  if (count > visualIndexLineCount) appendEstimatedVisualLines(visualIndexLineCount, count);
3848
4278
  }
3849
- function measureVisualLine(index) {
4279
+ function measureVisualLine(index, options) {
3850
4280
  ensureVisualIndex();
3851
4281
  if (index < 0 || index >= visualIndexLineCount) return 0;
3852
4282
  const key = lineKey(index);
3853
4283
  if (visualKeys[index] === key) return 0;
3854
- const base = props.style ?? defaultStyle.value;
3855
- const rows = props.ansi ? ansiWrappedRowsForLine(index, visualIndexLineCount, visualIndexWidth, base, styleCacheKey(base)) : wrappedRowsForLine(index, visualIndexLineCount, visualIndexWidth);
3856
- const nextCount = Math.max(1, rows.length);
4284
+ let nextCount;
4285
+ if (options?.countOnly === true) if (props.ansi) {
4286
+ const raw = props.source.getLine(index);
4287
+ const asciiCells = countAsciiAnsiTextCells(raw);
4288
+ nextCount = asciiCells == null ? countWrappedTextRows(ansiPlainTextForLine(index, visualIndexLineCount), visualIndexWidth) : Math.max(1, Math.ceil(asciiCells / visualIndexWidth));
4289
+ } else nextCount = countWrappedPlainTextRows(sanitizeInlineText(props.source.getLine(index)), visualIndexWidth);
4290
+ else {
4291
+ const base = props.style ?? defaultStyle.value;
4292
+ const rows = props.ansi ? ansiWrappedRowsForLine(index, visualIndexLineCount, visualIndexWidth, base, styleCacheKey(base)) : wrappedRowsForLine(index, visualIndexLineCount, visualIndexWidth);
4293
+ nextCount = Math.max(1, rows.length);
4294
+ }
3857
4295
  const prevCount = visualCounts[index] ?? 1;
3858
4296
  if (nextCount !== prevCount) {
3859
4297
  visualCounts[index] = nextCount;
@@ -3896,6 +4334,7 @@ const TLogView = defineComponent({
3896
4334
  if (visualMeasureCursor >= target) {
3897
4335
  syncVisualIndexStatus();
3898
4336
  emitVisualIndex();
4337
+ trimAnsiTextCache();
3899
4338
  return;
3900
4339
  }
3901
4340
  const started = ctx.now();
@@ -3907,7 +4346,7 @@ const TLogView = defineComponent({
3907
4346
  const effectiveTop = currentScrollTop() + topAdjustment;
3908
4347
  const start = visualStartForLine(index);
3909
4348
  const prevCount = visualCounts[index] ?? 1;
3910
- const delta = measureVisualLine(index);
4349
+ const delta = measureVisualLine(index, { countOnly: true });
3911
4350
  if (delta !== 0 && effectiveTop >= start + prevCount) topAdjustment += delta;
3912
4351
  visualMeasureCursor++;
3913
4352
  measuredLineCount = visualMeasureCursor;
@@ -3940,6 +4379,7 @@ const TLogView = defineComponent({
3940
4379
  }
3941
4380
  syncVisualIndexStatus();
3942
4381
  emitVisualIndex();
4382
+ trimAnsiTextCache();
3943
4383
  }
3944
4384
  function requestVisualIndexMeasurement(restartFrom = measuredLineCount) {
3945
4385
  if (!props.wrap) {
@@ -3953,6 +4393,7 @@ const TLogView = defineComponent({
3953
4393
  if (measuredLineCount >= target) {
3954
4394
  syncVisualIndexStatus();
3955
4395
  emitVisualIndex();
4396
+ trimAnsiTextCache();
3956
4397
  return;
3957
4398
  }
3958
4399
  const generation = ++visualMeasureGeneration;
@@ -4490,7 +4931,7 @@ const TLogView = defineComponent({
4490
4931
  if (options?.emitScroll) emitScroll(clampedTop);
4491
4932
  return true;
4492
4933
  }
4493
- const scrollPlan = !props.wrap ? prepareUnsafeFullRowScroll({
4934
+ const scrollPlan = prepareUnsafeFullRowScroll({
4494
4935
  render,
4495
4936
  plane: plane.value,
4496
4937
  rect: r,
@@ -4501,7 +4942,7 @@ const TLogView = defineComponent({
4501
4942
  isClipped: isClipped(),
4502
4943
  hasPendingDirtyRows: Boolean(dirtyRowsHint?.length),
4503
4944
  strategy
4504
- }) : null;
4945
+ });
4505
4946
  if (scrollPlan) {
4506
4947
  const rows = options?.extraDirtyRows?.length ? [...scrollPlan.exposedRows, ...options.extraDirtyRows] : scrollPlan.exposedRows;
4507
4948
  if (markRowsDirty(rows)) {
@@ -4981,14 +5422,12 @@ const TLogView = defineComponent({
4981
5422
  match: copySearchMatch(match)
4982
5423
  }));
4983
5424
  const count = lineCount();
4984
- const base = props.style ?? defaultStyle.value;
4985
- const baseStyleKey = styleCacheKey(base);
4986
5425
  const previewWidth = normalizeSearchResultPreviewWidth(options?.previewWidth);
4987
5426
  const contextCells = normalizeSearchResultContextCells(options?.contextCells);
4988
5427
  return searchMatches.slice(offset, offset + limit).map((match, index) => ({
4989
5428
  matchIndex: offset + index,
4990
5429
  match: copySearchMatch(match),
4991
- preview: previewForMatch(match, count, base, baseStyleKey, {
5430
+ preview: previewForMatch(match, count, {
4992
5431
  previewWidth,
4993
5432
  contextCells
4994
5433
  })