diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 88301d9ac7ec584ec9a8a822456a325300d671ad..72dc636bbeb467e7d156457495eb61d94a8fba75 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -501,6 +501,7 @@ export interface MultipleSelection { /** A select action on multiple locations. */ export interface MultipleSelect { + readonly index: number; readonly locations: Location[]; } @@ -546,7 +547,10 @@ function isSingleSelect(a: SelectionActions): a is SingleSelect { } function isMultipleSelect(a: SelectionActions): a is MultipleSelect { - return (a as MultipleSelect).locations !== undefined; + return ( + (a as MultipleSelect).locations !== undefined && + (a as MultipleSelect).index !== undefined + ); } function isNthSelect(a: SelectionActions): a is NthSelect { @@ -644,12 +648,13 @@ function reducer(s: Selection, action: SelectionActions): Selection { if (isMultipleSelect(action)) { if (action.locations.length === 0) return s; - const selection = selectLocation(s, action.locations[0]); + const index = action.index > 0 ? action.index : 0; + const selection = selectLocation(s, action.locations[index]); return { ...selection, multiple: { allSelections: action.locations, - index: 0, + index, }, }; } diff --git a/ivette/src/renderer/ASTview.tsx b/ivette/src/renderer/ASTview.tsx index ee63b4890abd8cee30e3a45756941a66244877de..ff2b3d97d473999cd9f6c85a31fdb1524f12ce16 100644 --- a/ivette/src/renderer/ASTview.tsx +++ b/ivette/src/renderer/ASTview.tsx @@ -3,6 +3,7 @@ // -------------------------------------------------------------------------- import React from 'react'; +import _ from 'lodash'; import * as Server from 'frama-c/server'; import * as States from 'frama-c/states'; @@ -57,8 +58,8 @@ async function loadAST( }); } catch (err) { PP.error( - 'Fail to retrieve the AST of function', theFunction, - 'marker:', theMarker, err, + `Fail to retrieve the AST of function '${theFunction}' ` + + `and marker '${theMarker}':`, err, ); } })(); @@ -72,7 +73,7 @@ async function functionCallers(functionName: string) { const locations = data.map(([fct, marker]) => ({ function: fct, marker })); return locations; } catch (err) { - PP.error(`Fail to retrieve callers of function ${functionName}.`, err); + PP.error(`Fail to retrieve callers of function '${functionName}':`, err); return []; } } @@ -91,7 +92,7 @@ const ASTview = () => { const [theme, setTheme] = Dome.useGlobalSetting('ASTview.theme', 'default'); const [fontSize, setFontSize] = Dome.useGlobalSetting('ASTview.fontSize', 12); const [wrapText, setWrapText] = Dome.useSwitch('ASTview.wrapText', false); - const markers = States.useSyncArray(markerInfo); + const markersInfo = States.useSyncArray(markerInfo); const theFunction = selection?.current?.function; const theMarker = selection?.current?.marker; @@ -129,26 +130,42 @@ const ASTview = () => { } } - function onContextMenu(id: key<'#markerInfo'>) { + async function onContextMenu(id: key<'#markerInfo'>) { const items = []; - const marker = markers.find((e) => e.key === id); - if (marker?.kind === 'function') { - items.push({ - label: `Go to definition of ${marker.name}`, - onClick: () => { - const location = { function: marker.name }; - updateSelection({ location }); - }, - }); - } - if (marker?.kind === 'declaration' && marker?.name) { - items.push({ - label: 'Go to callers', - onClick: async () => { - const locations = await functionCallers(marker.name); - updateSelection({ locations }); - }, - }); + const selectedMarkerInfo = markersInfo.find((e) => e.key === id); + switch (selectedMarkerInfo?.kind) { + case 'function': { + items.push({ + label: `Go to definition of ${selectedMarkerInfo.name}`, + onClick: () => { + const location = { function: selectedMarkerInfo.name }; + updateSelection({ location }); + }, + }); + break; + } + case 'declaration': { + if (selectedMarkerInfo?.name) { + const locations = await functionCallers(selectedMarkerInfo.name); + const locationsByFunction = _.groupBy(locations, (e) => e.function); + _.forEach(locationsByFunction, + (e) => { + const callerName = e[0].function; + items.push({ + label: + `Go to caller ${callerName} ` + + `${e.length > 1 ? `(${e.length} call sites)` : ''}`, + onClick: () => updateSelection({ + locations, + index: locations.findIndex((l) => l.function === callerName), + }), + }); + }); + } + break; + } + default: + break; } if (items.length > 0) Dome.popupMenu(items); diff --git a/ivette/src/renderer/Locations.tsx b/ivette/src/renderer/Locations.tsx index 375c4922e7030d48ba8a19c04c00d31beec649e2..3008d8d9cd2e83bd70089181f168d8f98578d9a2 100644 --- a/ivette/src/renderer/Locations.tsx +++ b/ivette/src/renderer/Locations.tsx @@ -25,18 +25,18 @@ const LocationsTable = () => { const model = React.useMemo(() => ( new CompactModel<number, LocationId>(({ id }: LocationId) => id) ), []); - const multiple: States.MultipleSelection = selection?.multiple; - const numberOfSelections = multiple?.allSelections?.length; + const multipleSelections = selection?.multiple; + const numberOfSelections = multipleSelections?.allSelections?.length; - // Updates [[model]] with the current multiple selection. + // Updates [[model]] with the current multiple selections. React.useEffect(() => { if (numberOfSelections > 0) { const data: LocationId[] = - multiple.allSelections.map((d, i) => ({ ...d, id: i })); + multipleSelections.allSelections.map((d, i) => ({ ...d, id: i })); model.replaceAllDataWith(data); } else model.clear(); - }, [numberOfSelections, multiple, model]); + }, [numberOfSelections, multipleSelections, model]); // Callbacks const onTableSelection = React.useCallback( @@ -45,7 +45,7 @@ const LocationsTable = () => { ); const reload = () => { - const location = multiple.allSelections[multiple.index]; + const location = multipleSelections.allSelections[multipleSelections.index]; updateSelection({ location }); }; @@ -62,7 +62,7 @@ const LocationsTable = () => { <IconButton icon="ANGLE.LEFT" onClick={() => updateSelection('MULTIPLE_PREV')} - enabled={numberOfSelections > 1 && multiple?.index > 0} + enabled={numberOfSelections > 1 && multipleSelections?.index > 0} title="Previous location" /> <IconButton @@ -70,7 +70,7 @@ const LocationsTable = () => { onClick={() => updateSelection('MULTIPLE_NEXT')} enabled={ numberOfSelections > 1 && - multiple?.index < numberOfSelections - 1 + multipleSelections?.index < numberOfSelections - 1 } title="Next location" /> @@ -82,8 +82,9 @@ const LocationsTable = () => { `location${numberOfSelections > 1 ? 's' : ''}` } > - {multiple?.allSelections.length === 0 ? - '0 / 0' : `${multiple?.index + 1} / ${numberOfSelections}`} + {multipleSelections?.allSelections.length === 0 ? + '0 / 0' : + `${multipleSelections?.index + 1} / ${numberOfSelections}`} </Label> <Space /> <IconButton @@ -95,7 +96,7 @@ const LocationsTable = () => { </TitleBar> <Table model={model} - selection={multiple?.index} + selection={multipleSelections?.index} onSelection={onTableSelection} > <Column @@ -118,9 +119,9 @@ const LocationsTable = () => { export default () => ( <Component - id="frama-c.selection" + id="frama-c.locations" label="Locations" - title="Browse a selection of multiple locations" + title="Browse multiple locations" > <LocationsTable /> </Component>