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/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx index 752eb7d40134aa193779fa71825ec7a6a1b4955c..5cde88b571d735ca163fc73cd1f20b6d2e761d13 100644 --- a/ivette/src/dome/renderer/frame/toolbars.tsx +++ b/ivette/src/dome/renderer/frame/toolbars.tsx @@ -383,7 +383,7 @@ function scrollToRef(r: null | HTMLLabelElement): void { function Suggestions(props: SuggestionsProps): JSX.Element { const { hints, onHint, index, onClose } = props; - const suggestions = hints.map((h, k) => { + const suggestions = hints.slice(0, 100).map((h, k) => { const selected = k === index || hints.length === 1; const classSelected = selected && 'dome-xToolBar-searchIndex'; const className = classes('dome-xToolBar-searchItem', classSelected); @@ -414,6 +414,9 @@ function Suggestions(props: SuggestionsProps): JSX.Element { onMouseDown={(event) => event.preventDefault()} > {suggestions} + {hints.length > 100 ? + <Label>({hints.length - 100} omitted)</Label> : + null} </div> ); } 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 b5ae15c210c051ddf574dc5aa4d8e62ed466d12e..1a6b90c4e2461462afe312c947c1975e27f733ba 100644 --- a/ivette/src/frama-c/kernel/Globals.tsx +++ b/ivette/src/frama-c/kernel/Globals.tsx @@ -31,6 +31,8 @@ import { classes } from 'dome/misc/utils'; 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'; @@ -40,13 +42,14 @@ 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 // -------------------------------------------------------------------------- function globalHints(): Ivette.Hint[] { const globals = States.getSyncArray(Ast.declAttributes).getArray(); - return globals.map((g : Ast.declAttributesData) => ({ + return globals.map((g: Ast.declAttributesData) => ({ id: g.decl, name: g.name, label: g.label, @@ -54,7 +57,7 @@ function globalHints(): Ivette.Hint[] { })); } -const globalMode : Ivette.SearchProps = { +const globalMode: Ivette.SearchProps = { id: 'frama-c.kernel.globals', label: 'Globals', title: 'Lookup for Global Declarations', @@ -94,15 +97,20 @@ 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; @@ -112,20 +120,39 @@ 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>; + 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 = + // @ts-expect-error (incompatibility due to @types/react versions) + <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 @@ -137,7 +164,7 @@ function List(props: ListProps): JSX.Element { summary={[count]} className='globals-section' > - {count > 0 ? children : total > 0 ? allFiltered : noItems} + {contents} </Section> ); } @@ -196,7 +223,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(); @@ -223,7 +252,7 @@ export function Functions(): JSX.Element { const multipleSelection: States.Scope[] = React.useMemo( () => markers.map((m) => getMarker(m)?.scope) - , [ getMarker, markers ]); + , [getMarker, markers]); const multipleSelectionActive = multipleSelection.length > 0; const evaComputed = States.useSyncValue(computationState) === 'computed'; @@ -245,9 +274,9 @@ export function Functions(): JSX.Element { && (extern[0] || !fct.extern) && (!multipleSelectionActive || !selected[0] || isSelected(fct)) && (evaAnalyzed[0] || !evaComputed || - !('eva_analyzed' in fct && fct.eva_analyzed === true)) + !('eva_analyzed' in fct && fct.eva_analyzed === true)) && (evaUnreached[0] || !evaComputed || - ('eva_analyzed' in fct && fct.eva_analyzed === true)); + ('eva_analyzed' in fct && fct.eva_analyzed === true)); return !!visible; } @@ -278,6 +307,7 @@ export function Functions(): JSX.Element { name="function" total={fcts.length} filteringMenuItems={contextMenuItems} + scrollableParent={props.scrollableParent} > {items} </List> @@ -304,7 +334,9 @@ function makeVarItem( ); } -export function Globals(): JSX.Element { +type VariablesProps = InfiniteScrollableListProps + +export function Variables(props: VariablesProps): JSX.Element { // Hooks const scope = States.useCurrentScope(); @@ -374,6 +406,7 @@ export function Globals(): JSX.Element { name="variable" total={variables.length} filteringMenuItems={contextMenuItems} + scrollableParent={props.scrollableParent} > {items} </List> @@ -410,7 +443,7 @@ function makeItem( } export function Declarations(props: DeclarationsProps): JSX.Element { - const { id, label, title, filter, defaultUnfold=false } = props; + const { id, label, title, filter, defaultUnfold = false } = props; const settings = React.useMemo(() => `frama-c.sidebar.${id}`, [id]); const data = States.useSyncArrayData(Ast.declAttributes); const scope = States.useCurrentScope(); @@ -440,7 +473,7 @@ export function Declarations(props: DeclarationsProps): JSX.Element { // -------------------------------------------------------------------------- const filterTypes = (d: Ast.declAttributesData): boolean => { - switch(d.kind) { + switch (d.kind) { case 'TYPE': case 'ENUM': case 'UNION': @@ -463,3 +496,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"