From 5897542b0f5334b10e5a51fda10596465361477b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Thu, 17 Dec 2020 14:34:36 +0100
Subject: [PATCH] [ivette/eva] stack layout

---
 ivette/src/dome/src/renderer/dome.tsx |  9 ++-
 ivette/src/frama-c/eva/Values.tsx     | 54 ++++++++++--------
 ivette/src/frama-c/eva/layout.ts      | 81 ++++++++++++++++++++-------
 ivette/src/frama-c/eva/model.ts       | 80 ++++++++++++++------------
 ivette/src/frama-c/eva/style.css      | 11 ++++
 5 files changed, 151 insertions(+), 84 deletions(-)

diff --git a/ivette/src/dome/src/renderer/dome.tsx b/ivette/src/dome/src/renderer/dome.tsx
index 237420fec39..12606069a35 100644
--- a/ivette/src/dome/src/renderer/dome.tsx
+++ b/ivette/src/dome/src/renderer/dome.tsx
@@ -490,11 +490,10 @@ export function useForceUpdate() {
 export function useUpdate(...events: Event<any>[]) {
   const fn = useForceUpdate();
   React.useEffect(() => {
-    if (events.length === 0) events.push(update);
-    events.forEach((evt) => evt.on(fn));
-    return () => events.forEach((evt) => evt.off(fn));
-  }, [fn, ...events]); // eslint-disable-line react-hooks/exhaustive-deps
-  // The rule signals events is missing, probably because of « … »
+    const theEvents = events ? events.slice() : [update];
+    theEvents.forEach((evt) => evt.on(fn));
+    return () => theEvents.forEach((evt) => evt.off(fn));
+  });
 }
 
 // --------------------------------------------------------------------------
diff --git a/ivette/src/frama-c/eva/Values.tsx b/ivette/src/frama-c/eva/Values.tsx
index 986e41886b3..120b58b4db5 100644
--- a/ivette/src/frama-c/eva/Values.tsx
+++ b/ivette/src/frama-c/eva/Values.tsx
@@ -24,7 +24,7 @@ import * as Ast from 'frama-c/api/kernel/ast';
 // Locals
 import { SizedArea, HSIZER, WSIZER } from './sized';
 import { sizeof } from './cells';
-import { RowKind } from './layout';
+import { Row } from './layout';
 import { Probe } from './probes';
 import { Model, getModelInstance } from './model';
 import './style.css';
@@ -35,7 +35,7 @@ import './style.css';
 
 function useModel(): Model {
   const model = getModelInstance();
-  Dome.useUpdate(model.signal);
+  Dome.useUpdate(model.changed, model.laidout);
   return model;
 }
 
@@ -90,8 +90,8 @@ function ProbeEditor() {
 // --------------------------------------------------------------------------
 
 interface TableCellProps {
-  kind: RowKind;
   probe: Probe;
+  row: Row;
 }
 
 const CELLPADDING = 4;
@@ -99,32 +99,36 @@ const CELLPADDING = 4;
 function TableCell(props: TableCellProps) {
   const model = useModel();
   const [selection, setSelection] = States.useSelection();
-  const { probe, kind } = props;
+  const { probe, row } = props;
+  const { kind, callstack } = row;
   const minWidth = CELLPADDING + WSIZER.dimension(probe.minCols);
   const maxWidth = CELLPADDING + WSIZER.dimension(probe.maxCols);
   const style = { width: minWidth, maxWidth };
-  let styling = 'dome-text-code';
   let contents: React.ReactNode = props.probe.marker;
   const { transient } = probe;
+
   switch (kind) {
+
+    // ---- Probe Contents
     case 'probes':
       if (transient) {
-        styling = 'dome-text-label';
-        contents = '« Probe »';
+        contents = <span className="dome-text-label">« Probe »</span>;
       } else {
         const { rank, code, label } = probe;
         const atpoint = rank && (
-          <span className='dome-text-code eva-probe-stmt'>@S{probe.rank}</span>
+          <span className="eva-probe-stmt">@S{rank}</span>
         );
-        styling = 'dome-text-label';
         contents = (
-          <>{label ?? code}{atpoint}</>
+          <span className="dome-text-label">{label ?? code}{atpoint}</span>
         );
       }
       break;
+
+    // ---- Values Contents
     case 'values':
+    case 'callstack':
       {
-        const { values } = model.values.getValues(probe.marker);
+        const { values } = model.values.getValues(probe.marker, callstack);
         const { cols, rows } = sizeof(values);
         contents = (
           <SizedArea cols={cols} rows={rows}>
@@ -133,11 +137,13 @@ function TableCell(props: TableCellProps) {
         );
       }
       break;
+
   }
+
+  // --- Cell Packing
   const isFocused = model.getFocused() === probe;
   const className = classes(
     'eva-cell',
-    styling,
     transient && 'eva-transient',
     !transient && isFocused && 'eva-focused',
   );
@@ -174,16 +180,23 @@ function TableRow(props: TableRowProps) {
   if (!row) return null;
   const { kind, probes } = row;
   const className = `eva-${kind}`;
+  const sk = row.stackIndex;
+  const header = row.stacks && (
+    <div className="eva-cell eva-stack">
+      {sk === undefined ? '#' : `${1 + sk}`}
+    </div>
+  );
   const contents = probes.map((probe) => (
     <TableCell
       key={probe.marker}
-      kind={kind}
       probe={probe}
+      row={row}
     />
   ));
   return (
     <Hpack className={className} style={props.style}>
       <div className="eva-row">
+        {header}
         {contents}
       </div>
       <Filler />
@@ -205,16 +218,13 @@ function ValuesPanel(props: Dimension) {
   const { width, height } = props;
   // --- reset line cache
   const listRef = React.useRef<VariableSizeList>(null);
-  const forceGridLayout = React.useCallback(
-    () => {
-      const vlist = listRef.current;
-      if (vlist) vlist.resetAfterIndex(0, true);
-    },
-    [listRef],
-  );
+  Dome.useEvent(model.laidout, () => {
+    const vlist = listRef.current;
+    if (vlist) vlist.resetAfterIndex(0, true);
+  });
   // --- compute line height
   const getRowHeight = React.useCallback(
-    (k: number) => HSIZER.dimension(model.getRowHeight(k)),
+    (k: number) => HSIZER.dimension(model.getRowLines(k)),
     [model],
   );
   // --- compute layout
@@ -223,7 +233,7 @@ function ValuesPanel(props: Dimension) {
   const [selection] = States.useSelection();
   React.useEffect(() => {
     const target = Ast.jMarker(selection?.current?.marker);
-    model.setLayout({ margin, target }, forceGridLayout);
+    model.setLayout({ margin, target });
   });
   // --- render list
   return (
diff --git a/ivette/src/frama-c/eva/layout.ts b/ivette/src/frama-c/eva/layout.ts
index e3a95b2dcb3..f060cf90cd6 100644
--- a/ivette/src/frama-c/eva/layout.ts
+++ b/ivette/src/frama-c/eva/layout.ts
@@ -2,8 +2,10 @@
 /* --- Layout                                                             ---*/
 /* --------------------------------------------------------------------------*/
 
-import { Size, EMPTY, addH, ValueCache } from './cells';
+import { callstack } from 'frama-c/api/plugins/eva/values';
 import { Probe } from './probes';
+import { StacksCache } from './stacks';
+import { Size, EMPTY, addH, ValueCache } from './cells';
 
 export interface LayoutProps {
   zoom?: number;
@@ -16,7 +18,11 @@ export interface Row {
   key: string;
   kind: RowKind;
   probes: Probe[];
-  height: number;
+  headstack?: string;
+  stacks?: number;
+  stackIndex?: number;
+  callstack?: callstack;
+  hlines: number;
 }
 
 /* --------------------------------------------------------------------------*/
@@ -31,16 +37,19 @@ export class LayoutEngine {
 
   // --- Setup
 
-  private readonly cache: ValueCache;
+  private readonly values: ValueCache;
+  private readonly stacks: StacksCache;
   private readonly hcrop: number;
   private readonly vcrop: number;
   private readonly margin: number;
 
   constructor(
-    cache: ValueCache,
     props: undefined | LayoutProps,
+    values: ValueCache,
+    stacks: StacksCache,
   ) {
-    this.cache = cache;
+    this.values = values;
+    this.stacks = stacks;
     const zoom = Math.max(0, props?.zoom ?? 0);
     this.vcrop = VCROP + 2 * zoom;
     this.hcrop = HCROP + zoom;
@@ -49,6 +58,7 @@ export class LayoutEngine {
   }
 
   // --- Probe Buffer
+  private byStacks?: string; // stmt
   private rowSize: Size = EMPTY;
   private buffer: Probe[] = [];
   private rows: Row[] = [];
@@ -61,33 +71,64 @@ export class LayoutEngine {
   }
 
   push(p: Probe) {
-    const probeSize = this.cache.getProbeSize(p.marker);
+    const probeSize = this.values.getProbeSize(p.marker);
     const s = this.crop(probeSize);
     p.minCols = s.cols;
     p.maxCols = Math.max(p.minCols, probeSize.cols);
-    if (s.cols + this.rowSize.cols > this.margin) this.flush();
+    const stmt = p.byCallstacks ? p.stmt : undefined;
+    if (stmt !== this.byStacks) {
+      this.flush();
+      this.byStacks = stmt;
+    }
+    if (!stmt && s.cols + this.rowSize.cols > this.margin)
+      this.flush();
     this.rowSize = addH(this.rowSize, s);
     this.rowSize.cols += PADDING;
     this.buffer.push(p);
   }
 
-  // --- Flush Buffer
+  // --- Flush Rows
+
   flush(): Row[] {
     const ps = this.buffer;
     const rs = this.rows;
     if (ps.length > 0) {
-      const n = rs.length;
-      rs.push({
-        key: `P${n}`,
-        kind: 'probes',
-        probes: ps,
-        height: 1,
-      }, {
-        key: `V${n}`,
-        kind: 'values',
-        probes: ps,
-        height: this.rowSize.rows,
-      });
+      const stmt = this.byStacks;
+      if (stmt) {
+        // --- by callstacks
+        const wcs = this.stacks.getStacks(stmt);
+        rs.push({
+          key: `P${stmt}`,
+          kind: 'probes',
+          probes: ps,
+          stacks: wcs.length,
+          hlines: 1,
+        });
+        wcs.forEach((cs, k) => {
+          rs.push({
+            key: `C${cs}`,
+            kind: 'callstack',
+            probes: ps,
+            stackIndex: k,
+            stacks: wcs.length,
+            hlines: this.values.getStackSize(cs).rows,
+          });
+        });
+      } else {
+        // --- by callstacks
+        const n = rs.length;
+        rs.push({
+          key: `P${n}`,
+          kind: 'probes',
+          probes: ps,
+          hlines: 1,
+        }, {
+          key: `V${n}`,
+          kind: 'values',
+          probes: ps,
+          hlines: this.rowSize.rows,
+        });
+      }
     }
     this.buffer = [];
     this.rowSize = EMPTY;
diff --git a/ivette/src/frama-c/eva/model.ts b/ivette/src/frama-c/eva/model.ts
index 7f749f28ed4..ef22d1d2eba 100644
--- a/ivette/src/frama-c/eva/model.ts
+++ b/ivette/src/frama-c/eva/model.ts
@@ -14,7 +14,7 @@ import * as Ast from 'frama-c/api/kernel/ast';
 // Model
 import { Probe } from './probes';
 import { StacksCache } from './stacks';
-import { callback, StateCallbacks, ValueCache } from './cells';
+import { StateCallbacks, ValueCache } from './cells';
 import { LayoutProps, LayoutEngine, Row } from './layout';
 
 export interface ModelLayout extends LayoutProps {
@@ -35,7 +35,7 @@ export class Model implements StateCallbacks {
     this.setLayout = throttle(this.setLayout.bind(this), 300);
     this.getRowKey = this.getRowKey.bind(this);
     this.getRowCount = this.getRowCount.bind(this);
-    this.getRowHeight = this.getRowHeight.bind(this);
+    this.getRowLines = this.getRowLines.bind(this);
     Server.onSignal(Values.changed, this.forceReload);
   }
 
@@ -76,6 +76,36 @@ export class Model implements StateCallbacks {
   private layout: ModelLayout = { margin: 80 };
   private rows: Row[] = [];
 
+  getRow(index: number): Row | undefined {
+    return this.rows[index];
+  }
+
+  getRowCount() {
+    return this.rows.length;
+  }
+
+  getRowKey(index: number): string {
+    const row = this.rows[index];
+    return row ? row.key : `#${index}`;
+  }
+
+  getRowLines(index: number): number {
+    const row = this.rows[index];
+    return row ? row.hlines : 0;
+  }
+
+  // --- Throttled
+  setLayout(ly: ModelLayout) {
+    if (!equal(this.layout, ly)) {
+      this.layout = ly;
+      const target = Ast.jMarker(ly.target);
+      this.selected = target && this.getProbe(target);
+      this.forceUpdate();
+    }
+  }
+
+  // --- Recompute Layout
+
   private computeLayout() {
     this.forcedLayout = false;
     const s = this.selected;
@@ -98,39 +128,14 @@ export class Model implements StateCallbacks {
         toLayout.push(p);
       }
     });
-    const engine = new LayoutEngine(this.values, this.layout);
+    const engine = new LayoutEngine(
+      this.layout,
+      this.values,
+      this.stacks,
+    );
     toLayout.sort(Probe.order).forEach(engine.push);
     this.rows = engine.flush();
-    this.forceUpdate();
-  }
-
-  getRow(index: number): Row | undefined {
-    return this.rows[index];
-  }
-
-  getRowCount() {
-    return this.rows.length;
-  }
-
-  getRowKey(index: number): string {
-    const row = this.rows[index];
-    return row ? row.key : `#${index}`;
-  }
-
-  getRowHeight(index: number): number {
-    const row = this.rows[index];
-    return row ? row.height : 0;
-  }
-
-  // --- Throttled
-  setLayout(ly: ModelLayout, forceGridLayout: callback) {
-    if (!equal(this.layout, ly)) {
-      this.layout = ly;
-      const target = Ast.jMarker(ly.target);
-      this.selected = target && this.getProbe(target);
-      this.forceLayout();
-      forceGridLayout();
-    }
+    this.laidout.emit();
   }
 
   // --- Force Reload (empty caches)
@@ -144,6 +149,10 @@ export class Model implements StateCallbacks {
     this.forceLayout();
   }
 
+  // --- Events
+  readonly changed = new Dome.Event('eva-changed');
+  readonly laidout = new Dome.Event('eva-laidout');
+
   // --- Force Layout
   forceLayout() {
     if (!this.forcedLayout) {
@@ -153,10 +162,7 @@ export class Model implements StateCallbacks {
   }
 
   // --- Foce Update
-  readonly signal = new Dome.Event('eva-force-update');
-  forceUpdate() {
-    this.signal.emit();
-  }
+  forceUpdate() { this.changed.emit(); }
 
 }
 
diff --git a/ivette/src/frama-c/eva/style.css b/ivette/src/frama-c/eva/style.css
index 7afd3a1d6c7..dea6379e551 100644
--- a/ivette/src/frama-c/eva/style.css
+++ b/ivette/src/frama-c/eva/style.css
@@ -71,6 +71,13 @@
 /* --- Table Cells                                                        --- */
 /* -------------------------------------------------------------------------- */
 
+.eva-stack {
+    width: 12px;
+    padding-top: 1px;
+    color: #777;
+    text-align: center;
+}
+
 .eva-cell {
     flex: 1 1 auto;
     border-left: thin solid black;
@@ -121,4 +128,8 @@
     background: #def6ff;
 }
 
+.eva-cell.eva-stack {
+    background: #eee;
+}
+
 /* -------------------------------------------------------------------------- */
-- 
GitLab