Make large diffs open quickly and scroll as one surface (#3212)
* perf(app): virtualize large diff rendering Render bounded diff row groups through one vertical list and one horizontal surface. Bound retained list ranges when expansion grows the data set so large diffs paint promptly without sacrificing scrolling behavior. * refactor(app): normalize working tree diff options * fix(app): keep wide diff text reachable
This commit is contained in:
@@ -1,58 +1,75 @@
|
||||
import { useState, useCallback, useEffect, useId, useRef } from "react";
|
||||
import { useState, useCallback, useEffect, useId } from "react";
|
||||
import { type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import {
|
||||
type LayoutChangeEvent,
|
||||
type NativeSyntheticEvent,
|
||||
type NativeScrollEvent,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gesture-handler";
|
||||
createAnimatedComponent,
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
type SharedValue,
|
||||
} from "react-native-reanimated";
|
||||
import { scheduleOnRN } from "react-native-worklets";
|
||||
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
||||
import { useFileExplorerCloseGestureRef } from "@/mobile-panels/gestures";
|
||||
|
||||
const AnimatedScrollView = createAnimatedComponent(ScrollView);
|
||||
|
||||
interface DiffScrollProps {
|
||||
children: React.ReactNode;
|
||||
scrollViewWidth: number;
|
||||
onScrollViewWidthChange: (width: number) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
offset: SharedValue<number>;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
export function DiffScroll({
|
||||
children,
|
||||
scrollViewWidth: _scrollViewWidth,
|
||||
onScrollViewWidthChange,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
offset,
|
||||
testID,
|
||||
}: DiffScrollProps) {
|
||||
const [isAtLeftEdge, setIsAtLeftEdge] = useState(true);
|
||||
const horizontalScroll = useHorizontalScrollOptional();
|
||||
const scrollId = useId();
|
||||
const scrollViewRef = useRef<ScrollViewType>(null);
|
||||
const [isAtLeftEdge, setIsAtLeftEdge] = useState(true);
|
||||
const isAtLeftEdgeValue = useSharedValue(true);
|
||||
|
||||
const closeGestureRef = useFileExplorerCloseGestureRef();
|
||||
|
||||
// Register/unregister scroll offset tracking
|
||||
useEffect(() => {
|
||||
if (!horizontalScroll) return;
|
||||
// Start at 0 (not scrolled)
|
||||
horizontalScroll.registerScrollOffset(scrollId, 0);
|
||||
return () => {
|
||||
horizontalScroll.unregisterScrollOffset(scrollId);
|
||||
};
|
||||
}, [horizontalScroll, scrollId]);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const offsetX = event.nativeEvent.contentOffset.x;
|
||||
// Track if we're at the left edge (with small threshold for float precision)
|
||||
setIsAtLeftEdge(offsetX <= 1);
|
||||
const recordScrollOffset = useCallback(
|
||||
(offsetX: number) => {
|
||||
if (horizontalScroll) {
|
||||
horizontalScroll.registerScrollOffset(scrollId, offsetX);
|
||||
}
|
||||
},
|
||||
[horizontalScroll, scrollId],
|
||||
);
|
||||
const recordLeftEdge = useCallback((atLeftEdge: boolean) => {
|
||||
setIsAtLeftEdge(atLeftEdge);
|
||||
}, []);
|
||||
|
||||
const handleScroll = useAnimatedScrollHandler({
|
||||
onScroll: (event) => {
|
||||
const offsetX = event.contentOffset.x;
|
||||
offset.value = offsetX;
|
||||
const atLeftEdge = offsetX <= 1;
|
||||
if (atLeftEdge !== isAtLeftEdgeValue.value) {
|
||||
isAtLeftEdgeValue.value = atLeftEdge;
|
||||
scheduleOnRN(recordLeftEdge, atLeftEdge);
|
||||
}
|
||||
scheduleOnRN(recordScrollOffset, offsetX);
|
||||
},
|
||||
});
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width),
|
||||
@@ -60,8 +77,7 @@ export function DiffScroll({
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
<AnimatedScrollView
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator
|
||||
@@ -71,6 +87,7 @@ export function DiffScroll({
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
onLayout={handleLayout}
|
||||
testID={testID}
|
||||
// When at left edge, wait for close gesture to fail before scrolling.
|
||||
// The close gesture fails quickly on leftward swipes (failOffsetX=-10),
|
||||
// so scrolling left works normally. On rightward swipes, close gesture
|
||||
@@ -78,6 +95,6 @@ export function DiffScroll({
|
||||
waitFor={isAtLeftEdge && closeGestureRef?.current ? closeGestureRef : undefined}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
</AnimatedScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user