mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
132 lines
3.5 KiB
TypeScript
132 lines
3.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
useRef,
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
useCallback,
|
|
useMemo,
|
|
} from 'react';
|
|
import type React from 'react';
|
|
import { VirtualizedList, type VirtualizedListRef } from './VirtualizedList.js';
|
|
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
|
import { Box, type DOMElement } from 'ink';
|
|
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
|
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
|
|
|
type VirtualizedListProps<T> = {
|
|
data: T[];
|
|
renderItem: (info: { item: T; index: number }) => React.ReactElement;
|
|
estimatedItemHeight: (index: number) => number;
|
|
keyExtractor: (item: T, index: number) => string;
|
|
initialScrollIndex?: number;
|
|
initialScrollOffsetInIndex?: number;
|
|
};
|
|
|
|
interface ScrollableListProps<T> extends VirtualizedListProps<T> {
|
|
hasFocus: boolean;
|
|
}
|
|
|
|
export type ScrollableListRef<T> = VirtualizedListRef<T>;
|
|
|
|
function ScrollableList<T>(
|
|
props: ScrollableListProps<T>,
|
|
ref: React.Ref<ScrollableListRef<T>>,
|
|
) {
|
|
const { hasFocus } = props;
|
|
const virtualizedListRef = useRef<VirtualizedListRef<T>>(null);
|
|
const containerRef = useRef<DOMElement>(null);
|
|
|
|
useImperativeHandle(
|
|
ref,
|
|
() => ({
|
|
scrollBy: (delta) => virtualizedListRef.current?.scrollBy(delta),
|
|
scrollTo: (offset) => virtualizedListRef.current?.scrollTo(offset),
|
|
scrollToEnd: () => virtualizedListRef.current?.scrollToEnd(),
|
|
scrollToIndex: (params) =>
|
|
virtualizedListRef.current?.scrollToIndex(params),
|
|
scrollToItem: (params) =>
|
|
virtualizedListRef.current?.scrollToItem(params),
|
|
getScrollIndex: () => virtualizedListRef.current?.getScrollIndex() ?? 0,
|
|
getScrollState: () =>
|
|
virtualizedListRef.current?.getScrollState() ?? {
|
|
scrollTop: 0,
|
|
scrollHeight: 0,
|
|
innerHeight: 0,
|
|
},
|
|
}),
|
|
[],
|
|
);
|
|
|
|
const getScrollState = useCallback(
|
|
() =>
|
|
virtualizedListRef.current?.getScrollState() ?? {
|
|
scrollTop: 0,
|
|
scrollHeight: 0,
|
|
innerHeight: 0,
|
|
},
|
|
[],
|
|
);
|
|
|
|
const scrollBy = useCallback((delta: number) => {
|
|
virtualizedListRef.current?.scrollBy(delta);
|
|
}, []);
|
|
|
|
const { scrollbarColor, flashScrollbar, scrollByWithAnimation } =
|
|
useAnimatedScrollbar(hasFocus, scrollBy);
|
|
|
|
useKeypress(
|
|
(key: Key) => {
|
|
if (key.shift) {
|
|
if (key.name === 'up') {
|
|
scrollByWithAnimation(-1);
|
|
}
|
|
if (key.name === 'down') {
|
|
scrollByWithAnimation(1);
|
|
}
|
|
}
|
|
},
|
|
{ isActive: hasFocus },
|
|
);
|
|
|
|
const hasFocusCallback = useCallback(() => hasFocus, [hasFocus]);
|
|
|
|
const scrollableEntry = useMemo(
|
|
() => ({
|
|
ref: containerRef as React.RefObject<DOMElement>,
|
|
getScrollState,
|
|
scrollBy: scrollByWithAnimation,
|
|
hasFocus: hasFocusCallback,
|
|
flashScrollbar,
|
|
}),
|
|
[getScrollState, scrollByWithAnimation, hasFocusCallback, flashScrollbar],
|
|
);
|
|
|
|
useScrollable(scrollableEntry, hasFocus);
|
|
|
|
return (
|
|
<Box
|
|
ref={containerRef}
|
|
flexGrow={1}
|
|
flexDirection="column"
|
|
overflow="hidden"
|
|
>
|
|
<VirtualizedList
|
|
ref={virtualizedListRef}
|
|
{...props}
|
|
scrollbarThumbColor={scrollbarColor}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const ScrollableListWithForwardRef = forwardRef(ScrollableList) as <T>(
|
|
props: ScrollableListProps<T> & { ref?: React.Ref<ScrollableListRef<T>> },
|
|
) => React.ReactElement;
|
|
|
|
export { ScrollableListWithForwardRef as ScrollableList };
|