Download packages/cli/src/ui/components/shared/ScrollableList.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 8.23 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/shared/ScrollableList.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/shared/ScrollableList.tsx
-
curl -L -o ScrollableList.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/shared/ScrollableList.tsx
8.23 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { | |
| useRef, | |
| forwardRef, | |
| useImperativeHandle, | |
| useCallback, | |
| useMemo, | |
| useLayoutEffect, | |
| } from 'react'; | |
| import type React from 'react'; | |
| import { | |
| VirtualizedList, | |
| type VirtualizedListRef, | |
| type VirtualizedListProps, | |
| SCROLL_TO_ITEM_END, | |
| } 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'; | |
| import { Command } from '../../key/keyMatchers.js'; | |
| import { useKeyMatchers } from '../../hooks/useKeyMatchers.js'; | |
| const ANIMATION_FRAME_DURATION_MS = 33; | |
| interface ScrollableListProps<T> extends VirtualizedListProps<T> { | |
| hasFocus: boolean; | |
| width?: string | number; | |
| scrollbar?: boolean; | |
| stableScrollback?: boolean; | |
| copyModeEnabled?: boolean; | |
| isStatic?: boolean; | |
| fixedItemHeight?: boolean; | |
| targetScrollIndex?: number; | |
| containerHeight?: number; | |
| scrollbarThumbColor?: string; | |
| } | |
| export type ScrollableListRef<T> = VirtualizedListRef<T>; | |
| function ScrollableList<T>( | |
| props: ScrollableListProps<T>, | |
| ref: React.Ref<ScrollableListRef<T>>, | |
| ) { | |
| const keyMatchers = useKeyMatchers(); | |
| const { hasFocus, width, scrollbar = true, stableScrollback } = 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); | |
| const smoothScrollState = useRef<{ | |
| active: boolean; | |
| start: number; | |
| from: number; | |
| to: number; | |
| duration: number; | |
| timer: NodeJS.Timeout | null; | |
| }>({ active: false, start: 0, from: 0, to: 0, duration: 0, timer: null }); | |
| const stopSmoothScroll = useCallback(() => { | |
| if (smoothScrollState.current.timer) { | |
| clearInterval(smoothScrollState.current.timer); | |
| smoothScrollState.current.timer = null; | |
| } | |
| smoothScrollState.current.active = false; | |
| }, []); | |
| useLayoutEffect(() => stopSmoothScroll, [stopSmoothScroll]); | |
| const smoothScrollTo = useCallback( | |
| ( | |
| targetScrollTop: number, | |
| duration: number = process.env['NODE_ENV'] === 'test' ? 0 : 200, | |
| ) => { | |
| stopSmoothScroll(); | |
| const scrollState = virtualizedListRef.current?.getScrollState() ?? { | |
| scrollTop: 0, | |
| scrollHeight: 0, | |
| innerHeight: 0, | |
| }; | |
| const { | |
| scrollTop: rawStartScrollTop, | |
| scrollHeight, | |
| innerHeight, | |
| } = scrollState; | |
| const maxScrollTop = Math.max(0, scrollHeight - innerHeight); | |
| const startScrollTop = Math.min(rawStartScrollTop, maxScrollTop); | |
| let effectiveTarget = targetScrollTop; | |
| if ( | |
| targetScrollTop === SCROLL_TO_ITEM_END || | |
| targetScrollTop >= maxScrollTop | |
| ) { | |
| effectiveTarget = maxScrollTop; | |
| } | |
| const clampedTarget = Math.max( | |
| 0, | |
| Math.min(maxScrollTop, effectiveTarget), | |
| ); | |
| if (duration === 0) { | |
| if ( | |
| targetScrollTop === SCROLL_TO_ITEM_END || | |
| targetScrollTop >= maxScrollTop | |
| ) { | |
| virtualizedListRef.current?.scrollTo(Number.MAX_SAFE_INTEGER); | |
| } else { | |
| virtualizedListRef.current?.scrollTo(Math.round(clampedTarget)); | |
| } | |
| flashScrollbar(); | |
| return; | |
| } | |
| smoothScrollState.current = { | |
| active: true, | |
| start: Date.now(), | |
| from: startScrollTop, | |
| to: clampedTarget, | |
| duration, | |
| timer: setInterval(() => { | |
| const now = Date.now(); | |
| const elapsed = now - smoothScrollState.current.start; | |
| const progress = Math.min(elapsed / duration, 1); | |
| // Ease-in-out | |
| const t = progress; | |
| const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; | |
| const current = | |
| smoothScrollState.current.from + | |
| (smoothScrollState.current.to - smoothScrollState.current.from) * | |
| ease; | |
| if (progress >= 1) { | |
| if ( | |
| targetScrollTop === SCROLL_TO_ITEM_END || | |
| targetScrollTop >= maxScrollTop | |
| ) { | |
| virtualizedListRef.current?.scrollTo(Number.MAX_SAFE_INTEGER); | |
| } else { | |
| virtualizedListRef.current?.scrollTo(Math.round(current)); | |
| } | |
| stopSmoothScroll(); | |
| flashScrollbar(); | |
| } else { | |
| virtualizedListRef.current?.scrollTo(Math.round(current)); | |
| } | |
| }, ANIMATION_FRAME_DURATION_MS), | |
| }; | |
| }, | |
| [stopSmoothScroll, flashScrollbar], | |
| ); | |
| useKeypress( | |
| (key: Key) => { | |
| if (keyMatchers[Command.SCROLL_UP](key)) { | |
| stopSmoothScroll(); | |
| scrollByWithAnimation(-1); | |
| return true; | |
| } else if (keyMatchers[Command.SCROLL_DOWN](key)) { | |
| stopSmoothScroll(); | |
| scrollByWithAnimation(1); | |
| return true; | |
| } else if ( | |
| keyMatchers[Command.PAGE_UP](key) || | |
| keyMatchers[Command.PAGE_DOWN](key) | |
| ) { | |
| const direction = keyMatchers[Command.PAGE_UP](key) ? -1 : 1; | |
| const scrollState = getScrollState(); | |
| const maxScroll = Math.max( | |
| 0, | |
| scrollState.scrollHeight - scrollState.innerHeight, | |
| ); | |
| const current = smoothScrollState.current.active | |
| ? smoothScrollState.current.to | |
| : Math.min(scrollState.scrollTop, maxScroll); | |
| const innerHeight = scrollState.innerHeight; | |
| smoothScrollTo(current + direction * innerHeight); | |
| return true; | |
| } else if (keyMatchers[Command.SCROLL_HOME](key)) { | |
| smoothScrollTo(0); | |
| return true; | |
| } else if (keyMatchers[Command.SCROLL_END](key)) { | |
| smoothScrollTo(SCROLL_TO_ITEM_END); | |
| return true; | |
| } | |
| return false; | |
| }, | |
| { isActive: hasFocus }, | |
| ); | |
| const hasFocusCallback = useCallback(() => hasFocus, [hasFocus]); | |
| const scrollableEntry = useMemo( | |
| () => ({ | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ref: containerRef as React.RefObject<DOMElement>, | |
| getScrollState, | |
| scrollBy: scrollByWithAnimation, | |
| scrollTo: smoothScrollTo, | |
| hasFocus: hasFocusCallback, | |
| flashScrollbar, | |
| }), | |
| [ | |
| getScrollState, | |
| hasFocusCallback, | |
| flashScrollbar, | |
| scrollByWithAnimation, | |
| smoothScrollTo, | |
| ], | |
| ); | |
| useScrollable(scrollableEntry, true); | |
| return ( | |
| <Box ref={containerRef} flexGrow={1} flexDirection="column" width={width}> | |
| <VirtualizedList | |
| ref={virtualizedListRef} | |
| {...props} | |
| scrollbar={scrollbar} | |
| scrollbarThumbColor={scrollbarColor} | |
| stableScrollback={stableScrollback} | |
| /> | |
| </Box> | |
| ); | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const ScrollableListWithForwardRef = forwardRef(ScrollableList) as <T>( | |
| props: ScrollableListProps<T> & { ref?: React.Ref<ScrollableListRef<T>> }, | |
| ) => React.ReactElement; | |
| export { ScrollableListWithForwardRef as ScrollableList }; | |