From a3c488c520b38a1d667ea9a8f8ad34e4cb1af463 Mon Sep 17 00:00:00 2001 From: Maxime Jacquemin <maxime2.jacquemin@gmail.com> Date: Wed, 18 Jan 2023 11:37:59 +0100 Subject: [PATCH] [Ivette] A new way to update a View Used to update the cursor position in ASTview when the marker is changed. Works like a charm. Also used to trigger gutters updates. --- ivette/src/dome/renderer/text/editor.tsx | 72 ++++++++++++-------- ivette/src/frama-c/kernel/ASTview.tsx | 83 ++++++------------------ 2 files changed, 66 insertions(+), 89 deletions(-) diff --git a/ivette/src/dome/renderer/text/editor.tsx b/ivette/src/dome/renderer/text/editor.tsx index c2f6cbdab53..5652c4638e0 100644 --- a/ivette/src/dome/renderer/text/editor.tsx +++ b/ivette/src/dome/renderer/text/editor.tsx @@ -24,7 +24,7 @@ import React from 'react'; import { EditorState, StateField, Facet, Extension } from '@codemirror/state'; import { Annotation, Transaction, RangeSet } from '@codemirror/state'; -import { EditorSelection, AnnotationType } from '@codemirror/state'; +import { EditorSelection } from '@codemirror/state'; import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; import { Decoration, DecorationSet } from '@codemirror/view'; @@ -56,8 +56,10 @@ export type View = EditorView | null; export type Range = { from: number, to: number }; export type Set<A> = (view: View, value: A) => void; export type Get<A> = (state: EditorState | undefined) => A; -export interface Structure<S> { structure: S, extension: Extension } -export interface Data<A, S> extends Structure<S> { init: A, get: Get<A> } +export type IsUpdated = (update: ViewUpdate) => boolean; +export interface Struct<S> { structure: S, extension: Extension } +export interface Value<A> { init: A, get: Get<A> } +export interface Data<A, S> extends Value<A>, Struct<S> { isUpdated: IsUpdated } // Event handlers type definition. export type Handler<I, E> = (i: I, v: EditorView, e: E) => void; @@ -72,7 +74,7 @@ export type Handlers<I> = { [e in keyof EventMap]?: Handler<I, EventMap[e]> }; // structure is exposed for two reasons. The first one is that it contains the // extension that must be added to the CodeMirror instanciation. The second one // is that it is needed during the Aspects creation's process. -export interface Field<A> extends Data<A, StateField<A>> { set: Set<A>, annotation: AnnotationType<A> } +export interface Field<A> extends Data<A, StateField<A>> { set: Set<A> } // An Aspect is a data associated with an editor state and computed by combining // data from several fields. A typical use case is if one needs a data that @@ -109,16 +111,23 @@ export type Dependencies<I extends Dict> = { [K in keyof I]: Dependency<I[K]> }; type Dep<A> = Dependency<A>; type Deps<I extends Dict> = Dependencies<I>; type Combine<Output> = (l: readonly Output[]) => Output; +type Pred<I extends Dict> = (d: Dep<I[typeof k]>, k: string) => boolean; type Mapper<I extends Dict, A> = (d: Dep<I[typeof k]>, k: string) => A; type Transform<I extends Dict> = Mapper<I, unknown>; // Helper function used to map a function over Dependencies. -function mapDict<I extends Dict, A>(deps: Deps<I>, fn: Mapper<I, A>): A[] { +function mapDeps<I extends Dict, A>(deps: Deps<I>, fn: Mapper<I, A>): A[] { return Object.keys(deps).map((k) => fn(deps[k], k)); } +// Helper function used to check if at least one depencency satisfied a +// given predicate. +function existsDeps<I extends Dict>(deps: Deps<I>, fn: Pred<I>): boolean { + return Object.keys(deps).find((k) => fn(deps[k], k)) != undefined; +} + // Helper function used to transfrom a Dependencies will keeping its structure. -function transformDict<I extends Dict>(deps: Deps<I>, tr: Transform<I>): Dict { +function transformDeps<I extends Dict>(deps: Deps<I>, tr: Transform<I>): Dict { return Object.fromEntries(Object.keys(deps).map(k => [k, tr(deps[k], k)])); } @@ -127,7 +136,7 @@ function transformDict<I extends Dict>(deps: Deps<I>, tr: Transform<I>): Dict { // type because of TypeScript subtyping shenanigans that prevent us to correctly // type the returned record. Thus, a type assertion has to be used. function inputs<I extends Dict>(ds: Deps<I>, s: EditorState | undefined): Dict { - return transformDict(ds, (d) => d.get(s)); + return transformDeps(ds, (d) => d.get(s)); } // ----------------------------------------------------------------------------- @@ -164,7 +173,9 @@ export function createField<A>(init: A): Field<A> { const field = StateField.define<A>({ create, update }); const get: Get<A> = (state) => state?.field(field) ?? init; const set: Set<A> = (v, a) => v?.dispatch({ annotations: annot.of(a) }); - return { init, get, set, structure: field, extension: field, annotation: annot }; + const isUpdated: IsUpdated = (update) => + update.transactions.find((tr) => tr.annotation(annot)) != undefined; + return { init, get, set, structure: field, extension: field, isUpdated }; } // An Aspect is declared using its dependencies and a function. This function's @@ -175,15 +186,17 @@ export function createAspect<I extends Dict, O>( deps: Dependencies<I>, fn: (input: I) => O, ): Aspect<O> { - const enables = mapDict(deps, (d) => d.extension); - const init = fn(transformDict(deps, (d) => d.init) as I); + const enables = mapDeps(deps, (d) => d.extension); + const init = fn(transformDeps(deps, (d) => d.init) as I); const combine: Combine<O> = (l) => l.length > 0 ? l[l.length - 1] : init; const facet = Facet.define<O, O>({ combine, enables }); const get: Get<O> = (state) => state?.facet(facet) ?? init; - const convertedDeps = mapDict(deps, (d) => d.structure); + const convertedDeps = mapDeps(deps, (d) => d.structure); const compute: Get<O> = (s) => fn(inputs(deps, s) as I); const extension = facet.compute(convertedDeps, compute); - return { init, get, structure: facet, extension }; + const isUpdated: IsUpdated = (update) => + existsDeps(deps, (d) => d.isUpdated(update)); + return { init, get, structure: facet, extension, isUpdated }; } // A Decorator is an extension that adds decorations to the CodeMirror's @@ -193,7 +206,7 @@ export function createDecorator<I extends Dict>( deps: Dependencies<I>, fn: (inputs: I, state: EditorState) => DecorationSet ): Extension { - const enables = mapDict(deps, (d) => d.extension); + const enables = mapDeps(deps, (d) => d.extension); const get = (s: EditorState): DecorationSet => fn(inputs(deps, s) as I, s); class S { s: DecorationSet = RangeSet.empty; } class D extends S { update(u: ViewUpdate): void { this.s = get(u.state); } } @@ -209,9 +222,10 @@ export function createGutter<I extends Dict>( className: string, line: (inputs: I, block: Range, view: EditorView) => GutterMarker | null ): Extension { - const enables = mapDict(deps, (d) => d.extension); + const enables = mapDeps(deps, (d) => d.extension); const extension = gutter({ class: className, + lineMarkerChange: (u) => existsDeps(deps, (d) => d.isUpdated(u)), lineMarker: (view, block) => { return line(inputs(deps, view.state) as I, block, view); } @@ -242,7 +256,7 @@ export function createEventHandler<I extends Dict>( deps: Dependencies<I>, handlers: Handlers<I>, ): Extension { - const enables = mapDict(deps, (d) => d.extension); + const enables = mapDeps(deps, (d) => d.extension); const domEventHandlers = Object.fromEntries(Object.keys(handlers).map((k) => { const h = handlers[k] as Handler<I, typeof k>; const fn = (e: typeof k, v: EditorView): void => @@ -252,8 +266,20 @@ export function createEventHandler<I extends Dict>( return enables.concat(EditorView.domEventHandlers(domEventHandlers)); } -export function createUpdater(fn: (update: ViewUpdate) => void): Extension { - return EditorView.updateListener.of(fn); +// A View updater is an extension that allows to modify the view each time a +// depencency is updated. For example, one could use this to change the cursor +// position when a Data is updated by the outside world. +export function createViewUpdater<I extends Dict>( + deps: Dependencies<I>, + fn: (input: I, view: View) => void, +): Extension { + return EditorView.updateListener.of((u) => { + if(!existsDeps(deps, (d) => d.isUpdated(u))) return; + const get = (b: boolean): EditorState => b ? u.state : u.startState; + const state: <X>(d: Dep<X>) => EditorState = (d) => get(d.isUpdated(u)); + const inputs = transformDeps(deps, (d) => d.get(state(d))) as I; + fn(inputs, u.view); + }); } // ----------------------------------------------------------------------------- @@ -318,6 +344,8 @@ function createSelectionField(): Field<EditorSelection> { export type ToString<A> = (text: A) => string; export function createTextField<A>(init: A, toString: ToString<A>): Field<A> { const field = createField<A>(init); + const isUpdated: IsUpdated = (u) => + field.isUpdated(u) && u.startState.doc.length !== 0; const set: Set<A> = (view, text) => { field.set(view, text); const selection = { anchor: 0 }; @@ -325,7 +353,7 @@ export function createTextField<A>(init: A, toString: ToString<A>): Field<A> { const changes = { from: 0, to: length, insert: toString(text) }; view?.dispatch({ changes, selection }); }; - return { ...field, set }; + return { ...field, set, isUpdated }; } // An extension displaying line numbers in a gutter. Does not display anything @@ -383,14 +411,6 @@ export function selectLine(view: View, line: number, atTop: boolean): void { view.dispatch({ effects }); } -export const TransactionExtenderTest = createTest(); -function createTest(): Extension { - return EditorState.transactionExtender.of((transaction) => { - console.log(transaction); - return null; - }); -} - // ----------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/kernel/ASTview.tsx b/ivette/src/frama-c/kernel/ASTview.tsx index ac8f6105960..ebc0419dfb9 100644 --- a/ivette/src/frama-c/kernel/ASTview.tsx +++ b/ivette/src/frama-c/kernel/ASTview.tsx @@ -149,27 +149,6 @@ function coveringNode(tree: Tree, pos: number): Node | undefined { // ----------------------------------------------------------------------------- -/* -function useViewState() { - const [selection, updateSelection] = States.useSelection(); - const [hovered, updateHovered] = States.useHovered(); - const selected = selection?.current?.marker; - const fct = selection?.current?.fct; - - const text = States.useRequest(Ast.printFunction, fct) ?? null; - const tree = React.useMemo(() => textToTree(text) ?? empty, [text]); - const ranges = React.useMemo(() => markersRanges(tree), [tree]); - const code = React.useMemo(() => textToString(text), [text]); - - const emptyDead = { unreachable: [], nonTerminating: [] }; - const dead = States.useRequest(Eva.getDeadCode, fct) ?? emptyDead; - const tags = States.useTags(Properties.propStatusTags); - const propertiesStatuses = States.useSyncArray(Properties.status).getArray(); - - -} -*/ - // ----------------------------------------------------------------------------- // Function code representation @@ -225,38 +204,20 @@ function createMarkerUpdater(): Editor.Extension { }); } -const MarkerScroller = Editor.createUpdater((update) => { - console.log(update); - const a = Marker.annotation; - const markers = mapFilter(update.transactions, (tr) => tr.annotation(a)); - if (markers.length !== 1) return; - const marker = markers[0]; - const selection = update.state.selection.main; - const ranges = Ranges.get(update.state).get(marker) ?? []; - if (ranges.length !== 1) { console.log(ranges); return; } - if (ranges[0] === selection) return; - const { from: anchor } = ranges[0]; - update.view.dispatch({ selection: { anchor }, scrollIntoView: true }); -}); - - -/* -// Scroll the selected marker into view if needed. Used for when the marker is -// changed outside of this component. -function scrollMarkerIntoView(view: Editor.View, marker: Marker): void { - if (!view || !marker) return; - const selection = view.state.selection.main; - console.log('-- Marker: ', marker); - const ranges = Ranges.get(view.state).get(marker) ?? []; - console.log('-- Ranges: ', ranges); - if (ranges.length === 0) return; - const exists = ranges.find((range) => range === selection); - console.log('-- Exists: ', exists); - if (exists) return; - const { from: anchor } = ranges[0]; - view.dispatch({ selection: { anchor }, scrollIntoView: true }); +// A View updater that scrolls the selected marker into view. It is needed to +// handle Marker's updates from the outside world, as they do not change the +// cursor position inside CodeMirror. +const MarkerScroller = createMarkerScroller(); +function createMarkerScroller(): Editor.Extension { + const deps = { marker: Marker, ranges: Ranges }; + return Editor.createViewUpdater(deps, ({ marker, ranges }, view) => { + if (!view || !marker) return; + const markerRanges = ranges.get(marker) ?? []; + if (markerRanges.length !== 1) return; + const { from: anchor } = markerRanges[0]; + view.dispatch({ selection: { anchor }, scrollIntoView: true }); + }); } -*/ // ----------------------------------------------------------------------------- @@ -660,7 +621,7 @@ function useFctTaints(fct: Fct): Eva.LvalueTaints[] { // Necessary extensions for our needs. const extensions: Editor.Extension[] = [ MarkerUpdater, - // MarkerScroller, + MarkerScroller, HoveredUpdater, CodeDecorator, DeadCodeDecorator, @@ -670,7 +631,6 @@ const extensions: Editor.Extension[] = [ TaintTooltip, Editor.FoldGutter, Editor.LanguageHighlighter, - Editor.TransactionExtenderTest, ]; // The component in itself. @@ -708,15 +668,12 @@ export default function ASTview(): JSX.Element { // they have changed. const text = useFctText(fct); React.useEffect(() => Text.set(view, text), [view, text]); - // const dead = useFctDead(fct); - // React.useEffect(() => Dead.set(view, dead), [view, dead]); - // const callers = useFctCallers(fct); - // React.useEffect(() => Callers.set(view, callers), [view, callers]); - // const taints = useFctTaints(fct); - // React.useEffect(() => TaintedLvalues.set(view, taints), [view, taints]); - - // Scrolling the selected marker into view if needed. - // React.useEffect(() => scrollMarkerIntoView(view, marker), [view, marker]); + const dead = useFctDead(fct); + React.useEffect(() => Dead.set(view, dead), [view, dead]); + const callers = useFctCallers(fct); + React.useEffect(() => Callers.set(view, callers), [view, callers]); + const taints = useFctTaints(fct); + React.useEffect(() => TaintedLvalues.set(view, taints), [view, taints]); return ( <> -- GitLab