diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index e0515681e67ad61d75bc699cc26c539f0f19abc6..0ed83e5c84573094db290629aba27b66218373cb 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -507,21 +507,112 @@ export function useSyncArray(id: string) { // --- Selection // -------------------------------------------------------------------------- +/** An AST location. */ +export interface Location { + /** Function name. */ + readonly function: string; + /** Marker identifier. */ + readonly marker?: string; +} + +export interface Selection { + /** Current selection. */ + current?: Location; + /** Previous locations with respect to the [[current]] one. */ + prevSelections: Location[]; + /** Next locations with respect to the [[current]] one. */ + nextSelections: Location[]; +} + +/** An action on a location. */ +export interface LocationAction { + /** Type of action: + * - `SELECT` selects a given [[location]]. + * - `GOTO` jumps to a given [[location]], and empties [[nextSelections]]. + */ + readonly type: 'SELECT' | 'GOTO'; + readonly location: Location; +} + +/** Actions on selection: + * - [[LocationAction]]. + * - `GO_BACK` jumps to previous location (first in [[prevSelections]]). + * - `GO_FORWARD` jumps to next location (first in [[nextSelections]]). + */ +export type SelectionActions = LocationAction | 'GO_BACK' | 'GO_FORWARD'; + +function isOnLocation(a: SelectionActions): a is LocationAction { + return (a as LocationAction).type !== undefined; +} + +/** Compute the next selection based on the current one and the given action. */ +function reducer(s: Selection, action: SelectionActions) { + if (isOnLocation(action)) { + switch (action.type) { + case 'SELECT': + // Save current location if the selected one is in a different function. + if (s.current?.function !== action.location.function && + (s.prevSelections.length !== 0 || s.nextSelections.length !== 0)) { + return { + current: action.location, + prevSelections: [s.current, ...s.prevSelections], + nextSelections: s.nextSelections, + }; + } + return { ...s, current: action.location }; + case 'GOTO': + return { + current: action.location, + prevSelections: [s.current, ...s.prevSelections], + nextSelections: [], + }; + default: + return s; + } + } else { + const [pS, ...prevS] = s.prevSelections; + const [nS, ...nextS] = s.nextSelections; + switch (action) { + case 'GO_BACK': + return { + current: pS, + prevSelections: prevS, + nextSelections: [s.current, ...s.nextSelections], + }; + case 'GO_FORWARD': + return { + current: nS, + prevSelections: [s.current, ...s.prevSelections], + nextSelections: nextS, + }; + default: + return s; + } + } +} + const SELECTION = 'kernel.selection'; -setStateDefault(SELECTION, {}); +const initialSelection = { + current: undefined, + prevSelections: [], + nextSelections: [], +}; +setStateDefault(SELECTION, initialSelection); /** - * @summary Current selection state. - * @return {array} `[selection,update]` for the current selection - * @description - * The selection is an object with many independant fields. - * You update it by providing only some fields, the other ones being kept - * unchanged, like the `setState()` behaviour of React components. + * Current selection. + * @return {array} The current selection and the function to update it. */ -export function useSelection() { - const [state, setState] = useState(SELECTION); - return [state, (upd: any) => setState({ ...state, ...upd })]; +export function useSelection(): [Selection, (a: SelectionActions) => void] { + const [selection, setSelection] = useState(SELECTION); + + function update(action: SelectionActions) { + const nextSelection = reducer(selection, action); + setSelection(nextSelection); + } + + return [selection, update]; } // -------------------------------------------------------------------------- diff --git a/ivette/src/renderer/ASTinfo.tsx b/ivette/src/renderer/ASTinfo.tsx index ec3106bddd7692a622ffa3f7c210f21acb0cb012..b2b1c16d21860e3160ef5c65e695c4e7328d096d 100644 --- a/ivette/src/renderer/ASTinfo.tsx +++ b/ivette/src/renderer/ASTinfo.tsx @@ -17,8 +17,8 @@ import { Component } from 'frama-c/LabViews'; const ASTinfo = () => { const buffer = React.useMemo(() => new RichTextBuffer(), []); - const [select, setSelect] = States.useSelection(); - const marker = select && select.marker; + const [selection, updateSelection] = States.useSelection(); + const marker = selection?.current?.marker; const data = States.useRequest( 'kernel.ast.info', marker, @@ -33,9 +33,10 @@ const ASTinfo = () => { }, [buffer, data]); // Callbacks - function onSelection(name: string) { + function onTextSelection(id: string) { // For now, the only markers are functions. - setSelect({ function: name, marker: undefined }); + const location = { function: id }; + updateSelection({ type: 'SELECT', location }); } // Component @@ -46,7 +47,7 @@ const ASTinfo = () => { buffer={buffer} mode="text" theme="default" - onSelection={onSelection} + onSelection={onTextSelection} readOnly /> </Vfill> diff --git a/ivette/src/renderer/ASTview.tsx b/ivette/src/renderer/ASTview.tsx index 4b85a415ae25464c08740c887589bd6d11158ce5..75506913f91ffd69cccd7b1680298b2a38a11444 100644 --- a/ivette/src/renderer/ASTview.tsx +++ b/ivette/src/renderer/ASTview.tsx @@ -72,15 +72,15 @@ const ASTview = () => { // Hooks const buffer = React.useMemo(() => new RichTextBuffer(), []); - const printed = React.useRef(); - const [select, setSelect] = States.useSelection(); + const printed: React.MutableRefObject<string | undefined> = React.useRef(); + const [selection, updateSelection] = States.useSelection(); 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('kernel.ast.markerKind'); - const theFunction = select && select.function; - const theMarker = select && select.marker; + const theFunction = selection?.current?.function; + const theMarker = selection?.current?.marker; // Hook: async loading React.useEffect(() => { @@ -96,18 +96,30 @@ const ASTview = () => { }, [buffer, theMarker]); // Callbacks + const doPrevSelect = () => { updateSelection('GO_BACK'); }; + const doNextSelect = () => { updateSelection('GO_FORWARD'); }; + const zoomIn = () => fontSize < 48 && setFontSize(fontSize + 2); const zoomOut = () => fontSize > 4 && setFontSize(fontSize - 2); - const onSelection = (marker: any) => setSelect({ marker }); - function contextMenu(id: string) { + function onTextSelection(id: string) { + if (selection.current) { + const location = { ...selection.current, marker: id }; + updateSelection({ type: 'SELECT', location }); + } + } + + function onContextMenu(id: string) { const marker = markers[id]; if (marker && marker.kind === 'function') { - const item1 = { + const item = { label: `Go to definition of ${marker.name}`, - onClick: () => setSelect({ function: marker.name, marker: undefined }), + onClick: () => { + const location = { function: marker.name }; + updateSelection({ type: 'GOTO', location }); + }, }; - Dome.popupMenu([item1]); + Dome.popupMenu([item]); } } @@ -122,6 +134,18 @@ const ASTview = () => { return ( <> <TitleBar> + <IconButton + icon="MEDIA.PREV" + onClick={doPrevSelect} + disabled={!selection || selection.prevSelections.length === 0} + title="Previous location" + /> + <IconButton + icon="MEDIA.NEXT" + onClick={doNextSelect} + disabled={!selection || selection.nextSelections.length === 0} + title="Next location" + /> <IconButton icon="ZOOM.OUT" onClick={zoomOut} @@ -153,8 +177,8 @@ const ASTview = () => { fontSize={fontSize} lineWrapping={wrapText} selection={theMarker} - onSelection={onSelection} - onContextMenu={contextMenu} + onSelection={onTextSelection} + onContextMenu={onContextMenu} readOnly /> </> diff --git a/ivette/src/renderer/Properties.tsx b/ivette/src/renderer/Properties.tsx index 3061204a39bbad89fbd9a1b5213c57263769514e..ed54c3d6d59163e27f9df6d526bd4264b884087b 100644 --- a/ivette/src/renderer/Properties.tsx +++ b/ivette/src/renderer/Properties.tsx @@ -87,17 +87,17 @@ class PropertyModel extends ArrayModel<Property> { const RenderTable = () => { // Hooks const model = React.useMemo(() => new PropertyModel(), []); - const items: { [key: string]: Property } = + const properties: { [key: string]: Property } = States.useSyncArray('kernel.properties'); const statusDict: { [status: string]: Tag } = States.useDictionary('kernel.dictionary.propstatus'); - const [select, setSelect] = + const [selection, updateSelection] = States.useSelection(); React.useEffect(() => { - const data = _.toArray(items); + const data = _.toArray(properties); model.replace(data); - }, [model, items]); + }, [model, properties]); // Callbacks const getStatus = React.useCallback( @@ -105,21 +105,24 @@ const RenderTable = () => { [statusDict], ); - const onSelection = React.useCallback( + const onPropertySelection = React.useCallback( ({ key, function: fct }: Property) => { - setSelect({ marker: key, function: fct }); - }, [setSelect], + if (fct) { + const location = { function: fct, marker: key }; + updateSelection({ type: 'SELECT', location }); + } + }, [updateSelection], ); - const selection = select?.marker; + const propertySelection = selection?.current?.marker; // Rendering return ( <Table<string, Property> model={model} sorting={model} - selection={selection} - onSelection={onSelection} + selection={propertySelection} + onSelection={onPropertySelection} settings="ivette.properties.table" > <ColumnCode id="function" label="Function" width={120} />