mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 14:43:37 -04:00
385 lines
17 KiB
Diff
385 lines
17 KiB
Diff
diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
|
|
index e470032a9572b3ced764ca02238a8c6be435a9d4..93770cdc02c570ce6aaa2ce256792b940c25e4d3 100644
|
|
--- a/dist/cjs/index.cjs
|
|
+++ b/dist/cjs/index.cjs
|
|
@@ -289,7 +289,7 @@ class Virtualizer {
|
|
let anchor = null;
|
|
let followOnAppend = null;
|
|
let edgeKeysChanged = false;
|
|
- if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && merged.anchorTo === "end" && this.scrollElement !== null) {
|
|
+ if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && this.scrollElement !== null) {
|
|
const prevCount = prevOptions.count;
|
|
const nextCount = merged.count;
|
|
const measurements = this.getMeasurements();
|
|
@@ -299,11 +299,20 @@ class Virtualizer {
|
|
const didEdgeKeysChange = didCountChange || prevCount > 0 && nextCount > 0 && (merged.getItemKey(0) !== prevFirstKey || merged.getItemKey(nextCount - 1) !== prevLastKey);
|
|
if (didEdgeKeysChange) {
|
|
edgeKeysChanged = true;
|
|
+ // A data change can legitimately re-key the rows around the current offset
|
|
+ // (e.g. a truncated leading chat turn regrouping once a prepended page loads
|
|
+ // its parent). Capture fallback anchors below the primary one so the scroll
|
|
+ // position survives even when the nearest keys disappear.
|
|
const item = prevCount > 0 ? this.getVirtualItemForOffset(this.getScrollOffset()) ?? measurements[0] : null;
|
|
if (item) {
|
|
- anchor = [item.key, this.getScrollOffset() - item.start];
|
|
+ anchor = [];
|
|
+ for (let i = item.index; i < prevCount && anchor.length < 100; i++) {
|
|
+ const candidate = measurements[i];
|
|
+ if (!candidate) break;
|
|
+ anchor.push([candidate.key, this.getScrollOffset() - candidate.start]);
|
|
+ }
|
|
}
|
|
- const behavior = merged.followOnAppend === true ? "auto" : merged.followOnAppend || null;
|
|
+ const behavior = merged.anchorTo === "end" ? merged.followOnAppend === true ? "auto" : merged.followOnAppend || null : null;
|
|
if (behavior && nextCount > prevCount && this.isAtEnd(prevOptions.scrollEndThreshold) && (prevCount === 0 || merged.getItemKey(nextCount - 1) !== prevLastKey)) {
|
|
followOnAppend = behavior;
|
|
}
|
|
@@ -316,30 +325,31 @@ class Virtualizer {
|
|
}
|
|
let anchorResolved = false;
|
|
let anchorDelta = 0;
|
|
- if (anchor && this.scrollOffset !== null) {
|
|
- const [anchorKey, anchorOffset] = anchor;
|
|
+ let resolvedAnchor = null;
|
|
+ if (anchor && anchor.length > 0 && this.scrollOffset !== null) {
|
|
const newMeasurements = this.getMeasurements();
|
|
const { count, getItemKey } = this.options;
|
|
- let idx = 0;
|
|
- while (idx < count && getItemKey(idx) !== anchorKey) {
|
|
- idx++;
|
|
- }
|
|
- if (idx < count) {
|
|
+ const indexByKey = new Map();
|
|
+ for (let i = 0; i < count; i++) indexByKey.set(getItemKey(i), i);
|
|
+ for (const [anchorKey, anchorOffset] of anchor) {
|
|
+ const idx = indexByKey.get(anchorKey);
|
|
+ if (idx === void 0) continue;
|
|
const anchorItem = newMeasurements[idx];
|
|
- if (anchorItem) {
|
|
- const newOffset = Math.max(0, anchorItem.start + anchorOffset);
|
|
- if (newOffset !== this.scrollOffset) {
|
|
- anchorDelta = newOffset - this.scrollOffset;
|
|
- this.scrollOffset = newOffset;
|
|
- anchorResolved = true;
|
|
- }
|
|
+ if (!anchorItem) continue;
|
|
+ resolvedAnchor = [anchorKey, anchorOffset];
|
|
+ const newOffset = Math.max(0, anchorItem.start + anchorOffset);
|
|
+ if (newOffset !== this.scrollOffset) {
|
|
+ anchorDelta = newOffset - this.scrollOffset;
|
|
+ this.scrollOffset = newOffset;
|
|
+ anchorResolved = true;
|
|
}
|
|
+ break;
|
|
}
|
|
}
|
|
if (anchorResolved || followOnAppend) {
|
|
this.pendingScrollAnchor = [
|
|
- anchorResolved ? anchor[0] : null,
|
|
- anchorResolved ? anchor[1] : 0,
|
|
+ anchorResolved ? resolvedAnchor[0] : null,
|
|
+ anchorResolved ? resolvedAnchor[1] : 0,
|
|
followOnAppend,
|
|
anchorDelta
|
|
];
|
|
@@ -725,17 +735,20 @@ class Virtualizer {
|
|
this.getMeasurements(),
|
|
this.getSize(),
|
|
this.getScrollOffset(),
|
|
- this.options.lanes
|
|
+ this.options.lanes,
|
|
+ this.options.paddingEnd
|
|
],
|
|
- (measurements, outerSize, scrollOffset, lanes) => {
|
|
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
|
|
if (measurements.length === 0 || outerSize === 0) {
|
|
this.range = null;
|
|
return null;
|
|
}
|
|
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
|
|
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
|
|
this.range = calculateRangeImpl(
|
|
measurements,
|
|
outerSize,
|
|
- scrollOffset,
|
|
+ effectiveScrollOffset,
|
|
lanes,
|
|
// Pass the typed array so binary search + forward-walk can read
|
|
// start/end directly from Float64Array, skipping the Proxy traps.
|
|
diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
|
|
index cc1001b8a14d06aa73a3a9e8f59870557ee9e7d6..70b202ed436ce1f3283a8465a81faf2e6e2e7983 100644
|
|
--- a/dist/cjs/index.d.cts
|
|
+++ b/dist/cjs/index.d.cts
|
|
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
|
|
startIndex: number;
|
|
endIndex: number;
|
|
} | null;
|
|
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
|
|
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
|
|
};
|
|
getVirtualIndexes: {
|
|
(): number[];
|
|
diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
|
|
index 6b43c0aea7ed9eeef75cbfb1351fcbd243913bdd..7be2680967934ddfbc4583a210a3d11ee2cddc94 100644
|
|
--- a/dist/esm/index.d.ts
|
|
+++ b/dist/esm/index.d.ts
|
|
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
|
|
startIndex: number;
|
|
endIndex: number;
|
|
} | null;
|
|
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
|
|
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
|
|
};
|
|
getVirtualIndexes: {
|
|
(): number[];
|
|
diff --git a/dist/esm/index.js b/dist/esm/index.js
|
|
index 2495b26cf2c3589213546b3958eaadf2eb6b751d..dc062edd6438f315bd8dddcf231986ad72158477 100644
|
|
--- a/dist/esm/index.js
|
|
+++ b/dist/esm/index.js
|
|
@@ -287,7 +287,7 @@ class Virtualizer {
|
|
let anchor = null;
|
|
let followOnAppend = null;
|
|
let edgeKeysChanged = false;
|
|
- if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && merged.anchorTo === "end" && this.scrollElement !== null) {
|
|
+ if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && this.scrollElement !== null) {
|
|
const prevCount = prevOptions.count;
|
|
const nextCount = merged.count;
|
|
const measurements = this.getMeasurements();
|
|
@@ -297,11 +297,20 @@ class Virtualizer {
|
|
const didEdgeKeysChange = didCountChange || prevCount > 0 && nextCount > 0 && (merged.getItemKey(0) !== prevFirstKey || merged.getItemKey(nextCount - 1) !== prevLastKey);
|
|
if (didEdgeKeysChange) {
|
|
edgeKeysChanged = true;
|
|
+ // A data change can legitimately re-key the rows around the current offset
|
|
+ // (e.g. a truncated leading chat turn regrouping once a prepended page loads
|
|
+ // its parent). Capture fallback anchors below the primary one so the scroll
|
|
+ // position survives even when the nearest keys disappear.
|
|
const item = prevCount > 0 ? this.getVirtualItemForOffset(this.getScrollOffset()) ?? measurements[0] : null;
|
|
if (item) {
|
|
- anchor = [item.key, this.getScrollOffset() - item.start];
|
|
+ anchor = [];
|
|
+ for (let i = item.index; i < prevCount && anchor.length < 100; i++) {
|
|
+ const candidate = measurements[i];
|
|
+ if (!candidate) break;
|
|
+ anchor.push([candidate.key, this.getScrollOffset() - candidate.start]);
|
|
+ }
|
|
}
|
|
- const behavior = merged.followOnAppend === true ? "auto" : merged.followOnAppend || null;
|
|
+ const behavior = merged.anchorTo === "end" ? merged.followOnAppend === true ? "auto" : merged.followOnAppend || null : null;
|
|
if (behavior && nextCount > prevCount && this.isAtEnd(prevOptions.scrollEndThreshold) && (prevCount === 0 || merged.getItemKey(nextCount - 1) !== prevLastKey)) {
|
|
followOnAppend = behavior;
|
|
}
|
|
@@ -314,30 +323,31 @@ class Virtualizer {
|
|
}
|
|
let anchorResolved = false;
|
|
let anchorDelta = 0;
|
|
- if (anchor && this.scrollOffset !== null) {
|
|
- const [anchorKey, anchorOffset] = anchor;
|
|
+ let resolvedAnchor = null;
|
|
+ if (anchor && anchor.length > 0 && this.scrollOffset !== null) {
|
|
const newMeasurements = this.getMeasurements();
|
|
const { count, getItemKey } = this.options;
|
|
- let idx = 0;
|
|
- while (idx < count && getItemKey(idx) !== anchorKey) {
|
|
- idx++;
|
|
- }
|
|
- if (idx < count) {
|
|
+ const indexByKey = new Map();
|
|
+ for (let i = 0; i < count; i++) indexByKey.set(getItemKey(i), i);
|
|
+ for (const [anchorKey, anchorOffset] of anchor) {
|
|
+ const idx = indexByKey.get(anchorKey);
|
|
+ if (idx === void 0) continue;
|
|
const anchorItem = newMeasurements[idx];
|
|
- if (anchorItem) {
|
|
- const newOffset = Math.max(0, anchorItem.start + anchorOffset);
|
|
- if (newOffset !== this.scrollOffset) {
|
|
- anchorDelta = newOffset - this.scrollOffset;
|
|
- this.scrollOffset = newOffset;
|
|
- anchorResolved = true;
|
|
- }
|
|
+ if (!anchorItem) continue;
|
|
+ resolvedAnchor = [anchorKey, anchorOffset];
|
|
+ const newOffset = Math.max(0, anchorItem.start + anchorOffset);
|
|
+ if (newOffset !== this.scrollOffset) {
|
|
+ anchorDelta = newOffset - this.scrollOffset;
|
|
+ this.scrollOffset = newOffset;
|
|
+ anchorResolved = true;
|
|
}
|
|
+ break;
|
|
}
|
|
}
|
|
if (anchorResolved || followOnAppend) {
|
|
this.pendingScrollAnchor = [
|
|
- anchorResolved ? anchor[0] : null,
|
|
- anchorResolved ? anchor[1] : 0,
|
|
+ anchorResolved ? resolvedAnchor[0] : null,
|
|
+ anchorResolved ? resolvedAnchor[1] : 0,
|
|
followOnAppend,
|
|
anchorDelta
|
|
];
|
|
@@ -723,17 +733,20 @@ class Virtualizer {
|
|
this.getMeasurements(),
|
|
this.getSize(),
|
|
this.getScrollOffset(),
|
|
- this.options.lanes
|
|
+ this.options.lanes,
|
|
+ this.options.paddingEnd
|
|
],
|
|
- (measurements, outerSize, scrollOffset, lanes) => {
|
|
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
|
|
if (measurements.length === 0 || outerSize === 0) {
|
|
this.range = null;
|
|
return null;
|
|
}
|
|
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
|
|
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
|
|
this.range = calculateRangeImpl(
|
|
measurements,
|
|
outerSize,
|
|
- scrollOffset,
|
|
+ effectiveScrollOffset,
|
|
lanes,
|
|
// Pass the typed array so binary search + forward-walk can read
|
|
// start/end directly from Float64Array, skipping the Proxy traps.
|
|
diff --git a/src/index.ts b/src/index.ts
|
|
index dc6f1010c4d4758de9c46fb8d69209e582e47171..5d8bf755e285e4d0688d5e41ed768c60e4267131 100644
|
|
--- a/src/index.ts
|
|
+++ b/src/index.ts
|
|
@@ -567,7 +567,7 @@ export class Virtualizer<
|
|
const prevOptions = this.options as
|
|
| Required<VirtualizerOptions<TScrollElement, TItemElement>>
|
|
| undefined
|
|
- let anchor: [Key, number] | null = null
|
|
+ let anchor: Array<[Key, number]> | null = null
|
|
let followOnAppend: ScrollBehavior | null = null
|
|
let edgeKeysChanged = false
|
|
|
|
@@ -575,7 +575,6 @@ export class Virtualizer<
|
|
prevOptions !== undefined &&
|
|
prevOptions.enabled &&
|
|
merged.enabled &&
|
|
- merged.anchorTo === 'end' &&
|
|
this.scrollElement !== null
|
|
) {
|
|
const prevCount = prevOptions.count
|
|
@@ -600,6 +599,10 @@ export class Virtualizer<
|
|
|
|
if (didEdgeKeysChange) {
|
|
edgeKeysChanged = true
|
|
+ // A data change can legitimately re-key the rows around the current offset
|
|
+ // (e.g. a truncated leading chat turn regrouping once a prepended page loads
|
|
+ // its parent). Capture fallback anchors below the primary one so the scroll
|
|
+ // position survives even when the nearest keys disappear.
|
|
const item =
|
|
prevCount > 0
|
|
? (this.getVirtualItemForOffset(this.getScrollOffset()) ??
|
|
@@ -607,13 +610,20 @@ export class Virtualizer<
|
|
: null
|
|
|
|
if (item) {
|
|
- anchor = [item.key, this.getScrollOffset() - item.start]
|
|
+ anchor = []
|
|
+ for (let i = item.index; i < prevCount && anchor.length < 100; i++) {
|
|
+ const candidate = measurements[i]
|
|
+ if (!candidate) break
|
|
+ anchor.push([candidate.key, this.getScrollOffset() - candidate.start])
|
|
+ }
|
|
}
|
|
|
|
const behavior =
|
|
- merged.followOnAppend === true
|
|
- ? 'auto'
|
|
- : merged.followOnAppend || null
|
|
+ merged.anchorTo === 'end'
|
|
+ ? merged.followOnAppend === true
|
|
+ ? 'auto'
|
|
+ : merged.followOnAppend || null
|
|
+ : null
|
|
|
|
if (
|
|
behavior &&
|
|
@@ -646,35 +656,36 @@ export class Virtualizer<
|
|
// frame, producing a visible "jump" on prepend with dynamic sizes.
|
|
let anchorResolved = false
|
|
let anchorDelta = 0
|
|
- if (anchor && this.scrollOffset !== null) {
|
|
- const [anchorKey, anchorOffset] = anchor
|
|
+ let resolvedAnchor: [Key, number] | null = null
|
|
+ if (anchor && anchor.length > 0 && this.scrollOffset !== null) {
|
|
const newMeasurements = this.getMeasurements()
|
|
const { count, getItemKey } = this.options
|
|
- let idx = 0
|
|
- while (idx < count && getItemKey(idx) !== anchorKey) {
|
|
- idx++
|
|
- }
|
|
- if (idx < count) {
|
|
+ const indexByKey = new Map<Key, number>()
|
|
+ for (let i = 0; i < count; i++) indexByKey.set(getItemKey(i), i)
|
|
+ for (const [anchorKey, anchorOffset] of anchor) {
|
|
+ const idx = indexByKey.get(anchorKey)
|
|
+ if (idx === undefined) continue
|
|
const anchorItem = newMeasurements[idx]
|
|
- if (anchorItem) {
|
|
- // Clamp to the reachable range's lower bound — anchorOffset may
|
|
- // have been derived from a transiently negative scrollOffset
|
|
- // (rubber-band), and a negative tracked offset never self-heals
|
|
- // when the element cannot scroll (#1229).
|
|
- const newOffset = Math.max(0, anchorItem.start + anchorOffset)
|
|
- if (newOffset !== this.scrollOffset) {
|
|
- anchorDelta = newOffset - this.scrollOffset
|
|
- this.scrollOffset = newOffset
|
|
- anchorResolved = true
|
|
- }
|
|
+ if (!anchorItem) continue
|
|
+ resolvedAnchor = [anchorKey, anchorOffset]
|
|
+ // Clamp to the reachable range's lower bound — anchorOffset may
|
|
+ // have been derived from a transiently negative scrollOffset
|
|
+ // (rubber-band), and a negative tracked offset never self-heals
|
|
+ // when the element cannot scroll (#1229).
|
|
+ const newOffset = Math.max(0, anchorItem.start + anchorOffset)
|
|
+ if (newOffset !== this.scrollOffset) {
|
|
+ anchorDelta = newOffset - this.scrollOffset
|
|
+ this.scrollOffset = newOffset
|
|
+ anchorResolved = true
|
|
}
|
|
+ break
|
|
}
|
|
}
|
|
|
|
if (anchorResolved || followOnAppend) {
|
|
this.pendingScrollAnchor = [
|
|
- anchorResolved ? anchor![0] : null,
|
|
- anchorResolved ? anchor![1] : 0,
|
|
+ anchorResolved ? resolvedAnchor![0] : null,
|
|
+ anchorResolved ? resolvedAnchor![1] : 0,
|
|
followOnAppend,
|
|
anchorDelta,
|
|
]
|
|
@@ -1410,16 +1421,25 @@ export class Virtualizer<
|
|
this.getSize(),
|
|
this.getScrollOffset(),
|
|
this.options.lanes,
|
|
+ this.options.paddingEnd,
|
|
],
|
|
- (measurements, outerSize, scrollOffset, lanes) => {
|
|
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
|
|
if (measurements.length === 0 || outerSize === 0) {
|
|
this.range = null
|
|
return null
|
|
}
|
|
+ const maxScrollOffset = Math.max(
|
|
+ this.options.scrollMargin + this.getTotalSize() - outerSize,
|
|
+ 0,
|
|
+ )
|
|
+ const effectiveScrollOffset = Math.min(
|
|
+ Math.max(scrollOffset, 0),
|
|
+ maxScrollOffset,
|
|
+ )
|
|
this.range = calculateRangeImpl(
|
|
measurements,
|
|
outerSize,
|
|
- scrollOffset,
|
|
+ effectiveScrollOffset,
|
|
lanes,
|
|
// Pass the typed array so binary search + forward-walk can read
|
|
// start/end directly from Float64Array, skipping the Proxy traps.
|