From 38074dbd55f70744aeff6feac38a2945d9cd49e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Mon, 11 Jan 2021 00:29:40 +0100 Subject: [PATCH] [ivette] extensible search hints API --- ivette/src/dome/renderer/frame/toolbars.tsx | 2 +- ivette/src/frama-c/states.ts | 32 +++++++- ivette/src/ivette/index.tsx | 83 ++++++++++++-------- ivette/src/ivette/prefs.tsx | 3 +- ivette/src/renderer/Application.tsx | 21 +++-- ivette/src/renderer/Extensions.tsx | 86 +++++++++++++++++++++ ivette/src/renderer/Globals.tsx | 48 ++++++------ ivette/tsconfig.json | 2 + ivette/webpack.renderer.js | 1 + 9 files changed, 202 insertions(+), 76 deletions(-) create mode 100644 ivette/src/renderer/Extensions.tsx diff --git a/ivette/src/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx index 74696bf0d3f..d84c50b4a32 100644 --- a/ivette/src/dome/renderer/frame/toolbars.tsx +++ b/ivette/src/dome/renderer/frame/toolbars.tsx @@ -242,7 +242,7 @@ export interface SearchFieldProps<A> { onSearch?: (pattern: string) => void; /** Hint selection callback. */ onHint?: (hint: Hint<A>) => void; - /** Event that triggers a focus request (defaults to [[Dome.find]]). */ + /** Event that triggers a focus request (defaults to [[dome.find]]). */ event?: null | Event<void>; } diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 1feb79a7c84..a7580038100 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -13,7 +13,7 @@ import * as Dome from 'dome'; import * as Json from 'dome/data/json'; import { Order } from 'dome/data/compare'; import { GlobalState, useGlobalState } from 'dome/data/states'; -import { useModel } from 'dome/table/models'; +import { Client, useModel } from 'dome/table/models'; import { CompactModel } from 'dome/table/arrays'; import * as Ast from 'frama-c/api/kernel/ast'; import * as Server from './server'; @@ -398,7 +398,7 @@ class SyncArray<K, A> { const syncArrays = new Map<string, SyncArray<any, any>>(); -function getSyncArray<K, A>( +function lookupSyncArray<K, A>( array: Array<K, A>, ): SyncArray<K, A> { const id = `${currentProject}@${array.name}`; @@ -418,7 +418,7 @@ Server.onShutdown(() => syncArrays.clear()); /** Force a Synchronized Array to reload. */ export function reloadArray<K, A>(arr: Array<K, A>) { - getSyncArray(arr).reload(); + lookupSyncArray(arr).reload(); } /** @@ -435,13 +435,37 @@ export function useSyncArray<K, A>( sync = true, ): CompactModel<K, A> { Dome.useUpdate(PROJECT); - const st = getSyncArray(arr); + const st = lookupSyncArray(arr); React.useEffect(st.update); Server.useSignal(arr.signal, st.fetch); useModel(st.model, sync); return st.model; } +/** + Return the associated array model. +*/ +export function getSyncArray<K, A>( + arr: Array<K, A>, +): CompactModel<K, A> { + const st = lookupSyncArray(arr); + return st.model; +} + +/** + Link on the associated array model. + @param onReload callback on reload event and update event if not specified. + @param onUpdate callback on update event. + */ +export function onSyncArray<K, A>( + arr: Array<K, A>, + onReload?: () => void, + onUpdate?: () => void, +): Client { + const st = lookupSyncArray(arr); + return st.model.link(onReload, onUpdate); +} + // -------------------------------------------------------------------------- // --- Selection // -------------------------------------------------------------------------- diff --git a/ivette/src/ivette/index.tsx b/ivette/src/ivette/index.tsx index d20e90d0bb9..3542f6c52ec 100644 --- a/ivette/src/ivette/index.tsx +++ b/ivette/src/ivette/index.tsx @@ -1,6 +1,6 @@ -// -------------------------------------------------------------------------- -// --- Lab View Component -// -------------------------------------------------------------------------- +/* --------------------------------------------------------------------------*/ +/* --- Lab View Component ---*/ +/* --------------------------------------------------------------------------*/ /** @packageDocumentation @@ -11,13 +11,8 @@ import React from 'react'; import { Label } from 'dome/controls/labels'; import { DefineElement } from 'dome/layout/dispatch'; import { GridItem, GridHbox, GridVbox } from 'dome/layout/grids'; -import { - Rankify, - useGroupContext, - useLibraryItem, - addLibraryItem, - useTitleContext, -} from 'ivette@lab'; +import * as Lab from 'ivette@lab'; +import * as Ext from 'ivette@ext'; /* --------------------------------------------------------------------------*/ /* --- Fragments ---*/ @@ -36,15 +31,15 @@ export interface FragmentProps { */ export function Fragment(props: FragmentProps) { const { group, rank, children } = props; - const context = useGroupContext(); + const context = Lab.useGroupContext(); const base = context.order ?? []; return ( - <Rankify + <Lab.Rankify group={group ?? context.group} order={rank === undefined ? base : [...base, rank]} > {children} - </Rankify> + </Lab.Rankify> ); } @@ -76,7 +71,7 @@ export interface ContentProps extends ItemProps { Empty groups are not displayed. */ export function registerGroup(group: ItemProps) { - addLibraryItem('groups', group); + Lab.addLibraryItem('groups', group); } /** @@ -89,20 +84,20 @@ export function registerGroup(group: ItemProps) { */ export function Group(props: ContentProps) { const { children, ...group } = props; - const context = useLibraryItem('groups', group); + const context = Lab.useLibraryItem('groups', group); return ( - <Rankify + <Lab.Rankify group={props.id} order={context.order ?? []} > {children} - </Rankify> + </Lab.Rankify> ); } -// -------------------------------------------------------------------------- -// --- View Layout -// -------------------------------------------------------------------------- +/* --------------------------------------------------------------------------*/ +/* --- View Layout ---*/ +/* --------------------------------------------------------------------------*/ export type Layout = string | { hsplit: Layout[] } | { vsplit: Layout[] }; @@ -146,7 +141,7 @@ export interface ViewLayoutProps extends ItemProps { /** Register a new View. */ export function registerView(view: ViewLayoutProps) { const { id, label, title, defaultView, layout } = view; - addLibraryItem('view', { + Lab.addLibraryItem('view', { id, label, title, @@ -155,9 +150,9 @@ export function registerView(view: ViewLayoutProps) { }); } -// -------------------------------------------------------------------------- -// --- Deprecated Views -// -------------------------------------------------------------------------- +/* --------------------------------------------------------------------------*/ +/* --- Deprecated View ---*/ +/* --------------------------------------------------------------------------*/ export interface ViewProps extends ContentProps { /** Use this view by default. */ @@ -180,13 +175,13 @@ export interface ViewProps extends ContentProps { @deprecated Use [[registerView]] instead. */ export function View(props: ViewProps) { - useLibraryItem('views', props); + Lab.useLibraryItem('views', props); return null; } -// -------------------------------------------------------------------------- -// --- Components -// -------------------------------------------------------------------------- +/* --------------------------------------------------------------------------*/ +/* --- Components ---*/ +/* --------------------------------------------------------------------------*/ export interface ComponentProps extends ContentProps { /** Group attachment. */ @@ -200,7 +195,7 @@ export interface ComponentProps extends ContentProps { Components are sorted by rank and identifier among each group. */ export function registerComponent(props: ComponentProps) { - addLibraryItem('components', props); + Lab.addLibraryItem('components', props); } /** @@ -212,7 +207,7 @@ export function registerComponent(props: ComponentProps) { @deprecated Use [[registerComponent]] instead. */ export function Component(props: ComponentProps) { - useLibraryItem('components', props); + Lab.useLibraryItem('components', props); return null; } @@ -240,7 +235,7 @@ export interface TitleBarProps { */ export function TitleBar(props: TitleBarProps) { const { icon, label, title, children } = props; - const context = useTitleContext(); + const context = Lab.useTitleContext(); if (!context.id) return null; return ( <DefineElement id={`labview.title.${context.id}`}> @@ -255,4 +250,30 @@ export function TitleBar(props: TitleBarProps) { ); } +/* --------------------------------------------------------------------------*/ +/* --- Search Hints ---*/ +/* --------------------------------------------------------------------------*/ + +export interface Hint { + id: string; + label: string | JSX.Element; + title?: string; + rank?: number; + onSelection: () => void; +} + +/** + Register a hint search engine for the Ivette toolbar. +*/ +export function registerHints( + id: string, + lookup: (pattern: string) => Promise<Hint[]>, +) { + const adaptor = (h: Hint): Ext.SearchHint => ( + { ...h, value: () => h.onSelection() } + ); + const search = (p: string) => lookup(p).then((hs) => hs.map(adaptor)); + Ext.registerHints({ id, search }); +} + // -------------------------------------------------------------------------- diff --git a/ivette/src/ivette/prefs.tsx b/ivette/src/ivette/prefs.tsx index 21fb66c7824..49a13dbb15e 100644 --- a/ivette/src/ivette/prefs.tsx +++ b/ivette/src/ivette/prefs.tsx @@ -2,8 +2,7 @@ // --- Main React Component rendered by './index.js' // -------------------------------------------------------------------------- -/* - Ivette Preferences +/** @packageDocumentation @module ivette/prefs */ diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx index 7abf1a04a01..346c0dfa820 100644 --- a/ivette/src/renderer/Application.tsx +++ b/ivette/src/renderer/Application.tsx @@ -14,7 +14,6 @@ import { GridHbox, GridItem } from 'dome/layout/grids'; // --- Ivette -import { LabView } from 'ivette@lab'; import { View, Group } from 'ivette'; // --- Frama-C @@ -29,7 +28,9 @@ import Values from 'frama-c/plugins/eva'; import Dive from 'frama-c/plugins/dive'; import * as Controller from './Controller'; -import Globals, { GlobalHint, useHints } from './Globals'; +import * as Extensions from './Extensions'; +import { LabView } from './LabView'; +import Globals from './Globals'; import 'frama-c/kernel/style.css'; @@ -70,13 +71,9 @@ export default (() => { Dome.useFlipSettings('frama-c.sidebar.unfold', true); const [viewbar, flipViewbar] = Dome.useFlipSettings('frama-c.viewbar.unfold', true); - const [hints, onSearchHint] = useHints(); - const [, setSelection] = States.useSelection(); - const onGlobalHint = (h: GlobalHint) => { - setSelection({ location: h.value }); - }; - const onSelectHint = () => { - if (hints.length === 1) onGlobalHint(hints[0]); + const hints = Extensions.useSearchHints(); + const onSelectedHints = () => { + if (hints.length === 1) Extensions.onSearchHint(hints[0]); }; return ( @@ -94,9 +91,9 @@ export default (() => { <Toolbar.SearchField placeholder="Search…" hints={hints} - onSearch={onSearchHint} - onSelect={onSelectHint} - onHint={onGlobalHint} + onSearch={Extensions.searchHints} + onHint={Extensions.onSearchHint} + onSelect={onSelectedHints} /> <Toolbar.Button icon="ITEMS.GRID" diff --git a/ivette/src/renderer/Extensions.tsx b/ivette/src/renderer/Extensions.tsx new file mode 100644 index 00000000000..5dab10ff7c5 --- /dev/null +++ b/ivette/src/renderer/Extensions.tsx @@ -0,0 +1,86 @@ +/* --------------------------------------------------------------------------*/ +/* --- Ivette Extensions ---*/ +/* --------------------------------------------------------------------------*/ + +import React from 'react'; +import * as Dome from 'dome'; +import { Hint } from 'dome/frame/toolbars'; + +/* --------------------------------------------------------------------------*/ +/* --- Search Hints ---*/ +/* --------------------------------------------------------------------------*/ + +export interface HintCallback { + (): void; +} + +export interface SearchHint extends Hint<HintCallback> { + rank?: number; +} + +function bySearchHint(a: SearchHint, b: SearchHint) { + const ra = a.rank ?? 0; + const rb = b.rank ?? 0; + if (ra < rb) return -1; + if (ra > rb) return +1; + return 0; +} + +export interface SearchEngine { + id: string; + search: (pattern: string) => Promise<SearchHint[]>; +} + +const NEWHINTS = new Dome.Event('ivette.hints'); +const HINTLOOKUP = new Map<string, SearchEngine>(); +const HINTS = new Map<string, SearchHint[]>(); +let CURRENT = ''; + +export function updateHints() { + if (CURRENT !== '') + NEWHINTS.emit(); +} + +export function registerHints(E: SearchEngine) { + HINTLOOKUP.set(E.id, E); +} + +export function searchHints(pattern: string) { + if (pattern === '') { + CURRENT = ''; + HINTS.clear(); + NEWHINTS.emit(); + } else { + const REF = pattern; + CURRENT = pattern; + HINTLOOKUP.forEach((E: SearchEngine) => { + E.search(REF).then((hs) => { + if (REF === CURRENT) { + HINTS.set(E.id, hs); + NEWHINTS.emit(); + } + }).catch(() => { + if (REF === CURRENT) { + HINTS.delete(E.id); + NEWHINTS.emit(); + } + }); + }); + } +} + +export function onSearchHint(h: SearchHint) { + h.value(); +} + +export function useSearchHints() { + const [hints, setHints] = React.useState<SearchHint[]>([]); + Dome.useEvent(NEWHINTS, () => { + let hs: SearchHint[] = []; + HINTS.forEach((rhs) => { hs = hs.concat(rhs); }); + setHints(hs.sort(bySearchHint)); + }); + return hints; +} + +/* --------------------------------------------------------------------------*/ diff --git a/ivette/src/renderer/Globals.tsx b/ivette/src/renderer/Globals.tsx index 0a4d754b4fd..341fb2e2d73 100644 --- a/ivette/src/renderer/Globals.tsx +++ b/ivette/src/renderer/Globals.tsx @@ -3,44 +3,39 @@ // -------------------------------------------------------------------------- import React from 'react'; -import { Section, Item } from 'dome/frame/sidebars'; -import type { Hint } from 'dome/frame/toolbars'; +import * as Dome from 'dome'; import { classes } from 'dome/misc/utils'; -import * as States from 'frama-c/states'; -import { useFlipSettings } from 'dome'; import { alpha } from 'dome/data/compare'; +import { Section, Item } from 'dome/frame/sidebars'; +import * as Ivette from 'ivette'; + +import * as States from 'frama-c/states'; import { functions, functionsData } from 'frama-c/api/kernel/ast'; import { isComputed } from 'frama-c/api/plugins/eva/general'; -import * as Dome from 'dome'; // -------------------------------------------------------------------------- // --- Global Search Hints // -------------------------------------------------------------------------- -export type GlobalHint = Hint<States.Location>; - -const makeHint = (fct: functionsData): GlobalHint => ({ - id: fct.key, - label: fct.name, - title: fct.signature, - value: { fct: fct.name }, -}); - -export function useHints(): [GlobalHint[], (pattern: string) => void] { - const fcts = States.useSyncArray(functions).getArray(); - const [hints, setHints] = React.useState<GlobalHint[]>([]); - const onSearch = (pattern: string) => { - if (pattern === '') setHints([]); - else { - const p = pattern.toLowerCase(); - setHints(fcts.filter((fn) => ( - 0 <= fn.name.toLowerCase().indexOf(p) - )).map(makeHint)); - } +function makeFunctionHint(fct: functionsData): Ivette.Hint { + return { + id: fct.key, + label: fct.name, + title: fct.signature, + onSelection: () => States.setSelection({ fct: fct.name }), }; - return [hints, onSearch]; } +async function lookupGlobals(pattern: string): Promise<Ivette.Hint[]> { + const lookup = pattern.toLowerCase(); + const fcts = States.getSyncArray(functions).getArray(); + return fcts.filter((fn) => ( + 0 <= fn.name.toLowerCase().indexOf(lookup) + )).map(makeFunctionHint); +} + +Ivette.registerHints('frama-c.globals', lookupGlobals); + // -------------------------------------------------------------------------- // --- Function Item // -------------------------------------------------------------------------- @@ -85,6 +80,7 @@ export default () => { const fcts = States.useSyncArray(functions).getArray().sort( (f, g) => alpha(f.name, g.name), ); + const { useFlipSettings } = Dome; const [stdlib, flipStdlib] = useFlipSettings('ivette.globals.stdlib', false); const [builtin, flipBuiltin] = diff --git a/ivette/tsconfig.json b/ivette/tsconfig.json index 1d61c4ca54a..18d37128204 100644 --- a/ivette/tsconfig.json +++ b/ivette/tsconfig.json @@ -43,6 +43,7 @@ "resolveJsonModule": true, /* Allow to load JSON files as module. */ "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + "ivette@ext": [ "src/renderer/Extensions.tsx" ], "ivette@lab": [ "src/renderer/LabView.tsx" ], "ivette": [ "src/ivette/index.tsx" ], "ivette/*": [ "src/ivette/*" ], @@ -104,6 +105,7 @@ "inputFiles": [ "doc/pages", "src/ivette/index.tsx", + "src/ivette/prefs.tsx", "src/frama-c/server.ts", "src/frama-c/states.ts", "src/frama-c/utils.ts", diff --git a/ivette/webpack.renderer.js b/ivette/webpack.renderer.js index 601b8b50dbd..6bd133984c3 100644 --- a/ivette/webpack.renderer.js +++ b/ivette/webpack.renderer.js @@ -29,6 +29,7 @@ module.exports = { alias: { 'frama-c/api': path.resolve( __dirname , 'src/frama-c/api/generated' ), 'frama-c': path.resolve( __dirname , 'src/frama-c' ), + 'ivette@ext': path.resolve( __dirname , 'src/renderer/Extensions' ), 'ivette@lab': path.resolve( __dirname , 'src/renderer/LabView' ), 'ivette': path.resolve( __dirname , 'src/ivette' ), 'dome/misc': path.resolve( DOME , 'misc' ), -- GitLab