diff --git a/ivette/package.json b/ivette/package.json index 88b867fbf7caa9cf4ba46ad42277eb724627bf7b..d111ed48493696ca2463aac7a4c9c07f987ecd02 100644 --- a/ivette/package.json +++ b/ivette/package.json @@ -46,6 +46,7 @@ "react-dom": "^18", "react-draggable": "^4.4.6", "react-fast-compare": "^3.2.2", + "react-infinite-scroller": "^1.2.6", "react-pivottable": "^0.11.0", "react-virtualized": "9.22.5", "react-virtualized-auto-sizer": "^1.0.22", @@ -63,6 +64,7 @@ "@types/node": "^18.19.9", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-infinite-scroller": "^1.2.5", "@types/react-virtualized": "^9.21.8", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", diff --git a/ivette/src/frama-c/index.tsx b/ivette/src/frama-c/index.tsx index 5e759842b4f30fa34f93884780214b18ec874c5f..33a5b74233eba561b64edf5bbdc5cf9aeea2496b 100644 --- a/ivette/src/frama-c/index.tsx +++ b/ivette/src/frama-c/index.tsx @@ -28,7 +28,7 @@ import React from 'react'; import * as Ivette from 'ivette'; import History from 'frama-c/kernel/History'; -import { Functions, Globals, Types } from 'frama-c/kernel/Globals'; +import Globals from 'frama-c/kernel/Globals'; import ASTview from 'frama-c/kernel/ASTview'; import ASTinfo from 'frama-c/kernel/ASTinfo'; import SourceCode from 'frama-c/kernel/SourceCode'; @@ -50,11 +50,7 @@ Menu.init(); Ivette.registerSidebar({ id: 'fc.kernel.globals', label: 'AST', - children: <> - <Types /> - <Globals /> - <Functions /> - </> + children: <Globals /> }); Ivette.registerToolbar({ diff --git a/ivette/src/frama-c/kernel/Globals.tsx b/ivette/src/frama-c/kernel/Globals.tsx index 53dc1942357f644d2dba65c21c43eda6116a4cd9..d2f52cf0a35ab36310feba36c24a3916b7f91b25 100644 --- a/ivette/src/frama-c/kernel/Globals.tsx +++ b/ivette/src/frama-c/kernel/Globals.tsx @@ -24,7 +24,7 @@ // --- Frama-C Globals // -------------------------------------------------------------------------- -import React, { ReactNode } from 'react'; +import React from 'react'; import * as Dome from 'dome'; import * as Json from 'dome/data/json'; import { classes } from 'dome/misc/utils'; @@ -32,6 +32,7 @@ import { alpha } from 'dome/data/compare'; import { Section, Item } from 'dome/frame/sidebars'; import { Button } from 'dome/controls/buttons'; import { Label } from 'dome/controls/labels'; +import InfiniteScroll from 'react-infinite-scroller'; import * as Ivette from 'ivette'; import * as Server from 'frama-c/server'; @@ -41,6 +42,7 @@ import * as Locations from 'frama-c/kernel/Locations'; import { computationState } from 'frama-c/plugins/eva/api/general'; import * as Eva from 'frama-c/plugins/eva/api/general'; + // -------------------------------------------------------------------------- // --- Global Search Hints // -------------------------------------------------------------------------- @@ -95,21 +97,22 @@ function menuItem(label: string, [b, flip]: setting, enabled?: boolean) // --- Lists // -------------------------------------------------------------------------- -interface ListProps { +interface InfiniteScrollableListProps { + scrollableParent: React.RefObject<HTMLDivElement>; +} + +type ListProps = { name: string; total: number; filteringMenuItems: Dome.PopupMenuItem[]; children: JSX.Element[]; -} +} & InfiniteScrollableListProps function List(props: ListProps): JSX.Element { - const { name, total, filteringMenuItems, children } = props; + const [displayedCount, setDisplayedCount] = React.useState(100); + const { name, total, filteringMenuItems, children, scrollableParent } = props; const Name = name.charAt(0).toUpperCase() + name.slice(1); const count = children.length; - const [slice, setSlice] = React.useState(1500); - React.useEffect(() => { - if (count < slice) setSlice(1500); - }, [count]); const filterButtonProps = { icon: 'TUNINGS', @@ -117,34 +120,38 @@ function List(props: ListProps): JSX.Element { onClick: () => Dome.popupMenu(filteringMenuItems), }; - const noItems = - <div className='dome-xSideBarSection-content'> - <label className='globals-info'> - There is no {name} to display. - </label> - </div>; - - const allFiltered = - <div className='dome-xSideBarSection-content'> - <label className='globals-info'> - All {name}s are filtered. Try adjusting {name} filters. - </label> - <Button {...filterButtonProps} label={`${Name}s filters`} /> - </div>; - - const omittedItems = - <div className='dome-xSideBarSection-content'> - <label className='globals-info'> - ({count - slice} omitted) - </label> - <Button - icon='CIRC.PLUS' - label="Show more" - onClick={() => setSlice(slice+500)} - /> - </div>; - - const items = count > slice ? children.slice(0, slice) : children; + let contents; + + if (count <= 0) { + contents = + <div className='dome-xSideBarSection-content'> + <label className='globals-info'> + All {name}s are filtered. Try adjusting {name} filters. + </label> + <Button {...filterButtonProps} label={`${Name}s filters`} /> + </div>; + } + else if (total <= 0) { + contents = + <div className='dome-xSideBarSection-content'> + <label className='globals-info'> + There is no {name} to display. + </label> + </div>; + } + else { + contents = + <InfiniteScroll + pageStart={0} + loadMore={() => setDisplayedCount(displayedCount + 100)} + hasMore={displayedCount < count} + loader={<Label key={-1}>Loading more...</Label>} + useWindow={false} + getScrollParent={() => scrollableParent.current} + > + {children.slice(0, displayedCount)} + </InfiniteScroll>; + } return ( <Section @@ -156,8 +163,7 @@ function List(props: ListProps): JSX.Element { summary={[count]} className='globals-section' > - {count > 0 ? items : total > 0 ? allFiltered : noItems} - {children.length > 1500 ? omittedItems : null} + {contents} </Section> ); } @@ -216,7 +222,9 @@ function computeFcts( return arr.sort((f, g) => alpha(f.name, g.name)); } -export function Functions(): JSX.Element { +type FunctionProps = InfiniteScrollableListProps + +export function Functions(props: FunctionProps): JSX.Element { // Hooks const scope = States.useCurrentScope(); @@ -287,11 +295,6 @@ export function Functions(): JSX.Element { menuItem('Selected only', selected, multipleSelectionActive), ]; - // Truncated - const allFunctions = - fcts.length > 1500 ? fcts.slice(0, 1500) : fcts; - const omitted = fcts.length - 1500; - // Filtered const items = fcts @@ -303,6 +306,7 @@ export function Functions(): JSX.Element { name="function" total={fcts.length} filteringMenuItems={contextMenuItems} + scrollableParent={props.scrollableParent} > {items} </List> @@ -329,7 +333,9 @@ function makeVarItem( ); } -export function Globals(): JSX.Element { +type VariablesProps = InfiniteScrollableListProps + +export function Variables(props: VariablesProps): JSX.Element { // Hooks const scope = States.useCurrentScope(); @@ -399,6 +405,7 @@ export function Globals(): JSX.Element { name="variable" total={variables.length} filteringMenuItems={contextMenuItems} + scrollableParent={props.scrollableParent} > {items} </List> @@ -488,3 +495,18 @@ export function Types(): JSX.Element { } // -------------------------------------------------------------------------- +// --- All globals +// -------------------------------------------------------------------------- + +export default function Globals(): JSX.Element { + const scrollableArea = React.useRef<HTMLDivElement>(null); + return ( + <div ref={scrollableArea} className="globals-scrollable-area"> + <Types /> + <Variables scrollableParent={scrollableArea} /> + <Functions scrollableParent={scrollableArea} /> + </div> + ); +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/kernel/style.css b/ivette/src/frama-c/kernel/style.css index d9e59c1d7b91c05ddc8f2e7d7a83d87b8b4e8f20..db351090b3fe8f2132e0ee0593be75505b230d35 100644 --- a/ivette/src/frama-c/kernel/style.css +++ b/ivette/src/frama-c/kernel/style.css @@ -64,6 +64,13 @@ /* --- Globals --- */ /* -------------------------------------------------------------------------- */ +.globals-scrollable-area { + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: scroll; +} + .globals-info { color: var(--info-text-discrete); font-style: italic; diff --git a/ivette/yarn.lock b/ivette/yarn.lock index 3637da3ac4476d9cf8d32b7b114fbd1da752c629..dff0d728be9d1aea58b9a657bd1bb6d6a7f3a1dd 100644 --- a/ivette/yarn.lock +++ b/ivette/yarn.lock @@ -984,6 +984,13 @@ dependencies: "@types/react" "*" +"@types/react-infinite-scroller@^1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.5.tgz#7c770be59465f3aaa1b86377d792d52de5e74047" + integrity sha512-fJU1jhMgoL6NJFrqTM0Ob7tnd2sQWGxe2ESwiU6FZWbJK/VO/Er5+AOhc+e2zbT0dk5pLygqctsulOLJ8xnSzw== + dependencies: + "@types/react" "*" + "@types/react-virtualized@^9.21.8": version "9.21.29" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.29.tgz#480b647f43a42f8e414d1af49a0ccd9b16537655" @@ -4577,7 +4584,7 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -prop-types@>=15.0.0, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@>=15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -4645,6 +4652,13 @@ react-fast-compare@^3.2.2: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== +react-infinite-scroller@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz#8b80233226dc753a597a0eb52621247f49b15f18" + integrity sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ== + dependencies: + prop-types "^15.5.8" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"