From f2864865c60671fea9f053300d02f96e9c05e5fa Mon Sep 17 00:00:00 2001
From: Michele Alberti <michele.alberti@cea.fr>
Date: Tue, 8 Dec 2020 16:36:39 +0100
Subject: [PATCH] [ivette] Add source code component.

---
 ivette/api/generated/kernel/ast/index.ts |  16 ++-
 ivette/src/renderer/ASTview.tsx          |  13 +-
 ivette/src/renderer/Application.tsx      |   2 +
 ivette/src/renderer/Preferences.tsx      |  70 +++++++++-
 ivette/src/renderer/SourceCode.tsx       | 165 +++++++++++++++++++++++
 5 files changed, 245 insertions(+), 21 deletions(-)
 create mode 100644 ivette/src/renderer/SourceCode.tsx

diff --git a/ivette/api/generated/kernel/ast/index.ts b/ivette/api/generated/kernel/ast/index.ts
index eaaf033326b..2a910fb94ae 100644
--- a/ivette/api/generated/kernel/ast/index.ts
+++ b/ivette/api/generated/kernel/ast/index.ts
@@ -129,8 +129,8 @@ export interface markerInfoData {
   name: string;
   /** Marker declaration or description */
   descr: string;
-  /** Marker position */
-  position: source;
+  /** Source location */
+  sloc: source;
 }
 
 /** Loose decoder for `markerInfoData` */
@@ -142,7 +142,7 @@ export const jMarkerInfoData: Json.Loose<markerInfoData> =
     var: jMarkerVarSafe,
     name: Json.jFail(Json.jString,'String expected'),
     descr: Json.jFail(Json.jString,'String expected'),
-    position: jSourceSafe,
+    sloc: jSourceSafe,
   });
 
 /** Safe decoder for `markerInfoData` */
@@ -153,13 +153,13 @@ export const jMarkerInfoDataSafe: Json.Safe<markerInfoData> =
 export const byMarkerInfoData: Compare.Order<markerInfoData> =
   Compare.byFields
     <{ key: Json.key<'#markerInfo'>, kind: markerKind, var: markerVar,
-       name: string, descr: string, position: source }>({
+       name: string, descr: string, sloc: source }>({
     key: Compare.string,
     kind: byMarkerKind,
     var: byMarkerVar,
     name: Compare.alpha,
     descr: Compare.string,
-    position: bySource,
+    sloc: bySource,
   });
 
 /** Signal for array [`markerInfo`](#markerinfo)  */
@@ -302,6 +302,8 @@ export interface functionsData {
   builtin?: boolean;
   /** Has the function been analyzed by Eva */
   eva_analyzed?: boolean;
+  /** Source location */
+  sloc: source;
 }
 
 /** Loose decoder for `functionsData` */
@@ -316,6 +318,7 @@ export const jFunctionsData: Json.Loose<functionsData> =
     stdlib: Json.jBoolean,
     builtin: Json.jBoolean,
     eva_analyzed: Json.jBoolean,
+    sloc: jSourceSafe,
   });
 
 /** Safe decoder for `functionsData` */
@@ -327,7 +330,7 @@ export const byFunctionsData: Compare.Order<functionsData> =
   Compare.byFields
     <{ key: Json.key<'#functions'>, name: string, signature: string,
        main?: boolean, defined?: boolean, stdlib?: boolean,
-       builtin?: boolean, eva_analyzed?: boolean }>({
+       builtin?: boolean, eva_analyzed?: boolean, sloc: source }>({
     key: Compare.string,
     name: Compare.alpha,
     signature: Compare.string,
@@ -336,6 +339,7 @@ export const byFunctionsData: Compare.Order<functionsData> =
     stdlib: Compare.defined(Compare.boolean),
     builtin: Compare.defined(Compare.boolean),
     eva_analyzed: Compare.defined(Compare.boolean),
+    sloc: bySource,
   });
 
 /** Signal for array [`functions`](#functions)  */
diff --git a/ivette/src/renderer/ASTview.tsx b/ivette/src/renderer/ASTview.tsx
index 4f50844ba06..e61bd279c6d 100644
--- a/ivette/src/renderer/ASTview.tsx
+++ b/ivette/src/renderer/ASTview.tsx
@@ -24,14 +24,7 @@ import 'codemirror/mode/clike/clike';
 import 'codemirror/theme/ambiance.css';
 import 'codemirror/theme/solarized.css';
 
-import { Theme, FontSize } from './Preferences';
-
-const THEMES = [
-  { id: 'default', label: 'Default' },
-  { id: 'ambiance', label: 'Ambiance' },
-  { id: 'solarized light', label: 'Solarized Light' },
-  { id: 'solarized dark', label: 'Solarized Dark' },
-];
+import { THEMES, ThemeASTview, FontSizeASTview } from './Preferences';
 
 // --------------------------------------------------------------------------
 // --- Pretty Printing (Browser Console)
@@ -110,8 +103,8 @@ const ASTview = () => {
   const printed = React.useRef<string | undefined>();
   const [selection, updateSelection] = States.useSelection();
   const multipleSelections = selection?.multiple.allSelections;
-  const [theme, setTheme] = Settings.useGlobalSettings(Theme);
-  const [fontSize, setFontSize] = Settings.useGlobalSettings(FontSize);
+  const [theme, setTheme] = Settings.useGlobalSettings(ThemeASTview);
+  const [fontSize, setFontSize] = Settings.useGlobalSettings(FontSizeASTview);
   const [wrapText, flipWrapText] = Dome.useFlipSettings('ASTview.wrapText');
   const markersInfo = States.useSyncArray(markerInfo);
 
diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx
index 8582d0b5005..381801a671a 100644
--- a/ivette/src/renderer/Application.tsx
+++ b/ivette/src/renderer/Application.tsx
@@ -23,6 +23,7 @@ import Globals, { GlobalHint, useHints } from './Globals';
 import Properties from './Properties';
 import Locations from './Locations';
 import Values from './Values';
+import SourceCode from './SourceCode';
 
 // --------------------------------------------------------------------------
 // --- Selection Controls
@@ -129,6 +130,7 @@ export default (() => {
           <Group id="frama-c" label="Frama-C" title="Frama-C Kernel Components">
             <Controller.Console />
             <Properties />
+            <SourceCode />
             <ASTview />
             <ASTinfo />
             <Locations />
diff --git a/ivette/src/renderer/Preferences.tsx b/ivette/src/renderer/Preferences.tsx
index 02a7a602d3b..7fa51940e2b 100644
--- a/ivette/src/renderer/Preferences.tsx
+++ b/ivette/src/renderer/Preferences.tsx
@@ -16,16 +16,27 @@ import React from 'react';
 import * as Settings from 'dome/data/settings';
 import * as Forms from 'dome/layout/forms';
 
-export const Theme = new Settings.GString('ASTview.theme', 'default');
-export const FontSize = new Settings.GNumber('ASTview.fontSize', 12);
+export const THEMES = [
+  { id: 'default', label: 'Default' },
+  { id: 'ambiance', label: 'Ambiance' },
+  { id: 'solarized light', label: 'Solarized Light' },
+  { id: 'solarized dark', label: 'Solarized Dark' },
+];
+
+// --------------------------------------------------------------------------
+// --- AST View Preferences
+// --------------------------------------------------------------------------
+
+export const ThemeASTview = new Settings.GString('ASTview.theme', 'default');
+export const FontSizeASTview = new Settings.GNumber('ASTview.fontSize', 12);
 
 const ASTviewPrefs = () => {
 
   const theme = Forms.useDefined(Forms.useValid(
-    Settings.useGlobalSettings(Theme),
+    Settings.useGlobalSettings(ThemeASTview),
   ));
   const font = Forms.useValid(
-    Settings.useGlobalSettings(FontSize),
+    Settings.useGlobalSettings(FontSizeASTview),
   );
 
   return (
@@ -54,6 +65,55 @@ const ASTviewPrefs = () => {
   );
 };
 
+// --------------------------------------------------------------------------
+// --- Source Code Preferences
+// --------------------------------------------------------------------------
+
+export const ThemeSC = new Settings.GString('SourceCode.theme', 'default');
+export const FontSizeSC = new Settings.GNumber('SourceCode.fontSize', 12);
+
+const SourceCodePrefs = () => {
+
+  const theme = Forms.useDefined(Forms.useValid(
+    Settings.useGlobalSettings(ThemeSC),
+  ));
+  const font = Forms.useValid(
+    Settings.useGlobalSettings(FontSizeSC),
+  );
+
+  return (
+    <Forms.Page>
+      <Forms.Section label="Source Code" unfold>
+        <Forms.SelectField
+          state={theme}
+          label="Theme"
+          title="Set the color theme of the source code"
+        >
+          <option value="default" label="Default" />
+          <option value="ambiance" label="Ambiance" />
+          <option value="solarized light" label="Solarized light" />
+          <option value="solarized dark" label="Solarized dark" />
+        </Forms.SelectField>
+        <Forms.SliderField
+          state={font}
+          label="Font Size"
+          title="Set the font size of the source code"
+          min={8}
+          max={32}
+          step={2}
+        />
+      </Forms.Section>
+    </Forms.Page>
+  );
+};
+
+// --------------------------------------------------------------------------
+// --- Export Components
+// --------------------------------------------------------------------------
+
 export default (() => (
-  <ASTviewPrefs />
+  <>
+    <ASTviewPrefs />
+    <SourceCodePrefs />
+  </>
 ));
diff --git a/ivette/src/renderer/SourceCode.tsx b/ivette/src/renderer/SourceCode.tsx
new file mode 100644
index 00000000000..f175c2ef2ed
--- /dev/null
+++ b/ivette/src/renderer/SourceCode.tsx
@@ -0,0 +1,165 @@
+// --------------------------------------------------------------------------
+// --- Source Code
+// --------------------------------------------------------------------------
+
+import React from 'react';
+import _ from 'lodash';
+import * as States from 'frama-c/states';
+
+import * as Dome from 'dome';
+import { readFile } from 'dome/system';
+import * as Json from 'dome/data/json';
+import * as Settings from 'dome/data/settings';
+import { RichTextBuffer } from 'dome/text/buffers';
+import { Text } from 'dome/text/editors';
+import { IconButton } from 'dome/controls/buttons';
+import { Component, TitleBar } from 'frama-c/LabViews';
+import { functions, markerInfo } from 'frama-c/api/kernel/ast';
+import { source } from 'frama-c/api/kernel/services';
+
+import 'codemirror/mode/clike/clike';
+import 'codemirror/theme/ambiance.css';
+import 'codemirror/theme/solarized.css';
+import 'codemirror/addon/selection/active-line';
+import 'codemirror/addon/dialog/dialog.css';
+import 'codemirror/addon/dialog/dialog';
+import 'codemirror/addon/search/searchcursor';
+import 'codemirror/addon/search/search';
+import 'codemirror/addon/search/jump-to-line';
+
+import { THEMES, ThemeSC, FontSizeSC } from './Preferences';
+
+// --------------------------------------------------------------------------
+// --- Pretty Printing (Browser Console)
+// --------------------------------------------------------------------------
+
+const D = new Dome.Debug('Source Code');
+
+// --------------------------------------------------------------------------
+// --- Rich Text Printer
+// --------------------------------------------------------------------------
+
+async function loadSourceCode(buffer: RichTextBuffer, sloc: source) {
+  const { file, line } = sloc;
+  try {
+    const content = await readFile(file);
+    buffer.setValue(content);
+    buffer.scroll(line);
+    buffer.getDoc().setCursor(line);
+  } catch (err) {
+    D.error(`Fail to load source code file ${file}.`);
+  }
+}
+
+// --------------------------------------------------------------------------
+// --- Source Code Printer
+// --------------------------------------------------------------------------
+
+const SourceCode = () => {
+
+  // Hooks
+  const buffer = React.useMemo(() => new RichTextBuffer(), []);
+  const [selection] = States.useSelection();
+  const [theme, setTheme] = Settings.useGlobalSettings(ThemeSC);
+  const [fontSize, setFontSize] = Settings.useGlobalSettings(FontSizeSC);
+  const [wrapText, flipWrapText] = Dome.useFlipSettings('SourceCode.wrapText');
+
+  const markersInfo = States.useSyncArray(markerInfo);
+  const fcts = States.useSyncArray(functions).getArray();
+
+  const theFunction = selection?.current?.function;
+  const currentFunction = React.useRef<string | undefined>();
+
+  const theMarker = selection?.current?.marker;
+  const currentMarker = React.useRef<string | undefined>();
+
+  // Hook: async loading
+  React.useEffect(() => {
+    if (theMarker && currentMarker.current !== theMarker) {
+      currentMarker.current = theMarker;
+      const markerId = (theMarker as Json.key<'#markerInfo'>);
+      const markerIdInfo = markersInfo.getData(markerId);
+      if (markerIdInfo) {
+        loadSourceCode(buffer, markerIdInfo.sloc);
+      }
+    } else if (theFunction && currentFunction.current !== theFunction) {
+      currentFunction.current = theFunction;
+      const currentFunctionData = _.find(fcts, (e) => e.name === theFunction);
+      if (currentFunctionData) {
+        loadSourceCode(buffer, currentFunctionData.sloc);
+      }
+    } else
+      buffer.clear();
+  }, [buffer, fcts, markersInfo, theFunction, theMarker]);
+
+  // Callbacks
+  const zoomIn = () => fontSize < 48 && setFontSize(fontSize + 2);
+  const zoomOut = () => fontSize > 4 && setFontSize(fontSize - 2);
+
+  // Theme Popup
+  const selectTheme = (id?: string) => id && setTheme(id);
+  const themeItem = (th: { id: string; label: string }) => (
+    { checked: th.id === theme, ...th }
+  );
+  const themePopup = () => Dome.popupMenu(THEMES.map(themeItem), selectTheme);
+
+  // Component
+  return (
+    <>
+      <TitleBar>
+        <IconButton
+          icon="ZOOM.OUT"
+          onClick={zoomOut}
+          disabled={!theFunction}
+          title="Decrease font size"
+        />
+        <IconButton
+          icon="ZOOM.IN"
+          onClick={zoomIn}
+          disabled={!theFunction}
+          title="Increase font size"
+        />
+        <IconButton
+          icon="PAINTBRUSH"
+          onClick={themePopup}
+          title="Choose theme"
+        />
+        <IconButton
+          icon="WRAPTEXT"
+          selected={wrapText}
+          onClick={flipWrapText}
+          title="Wrap text"
+        />
+      </TitleBar>
+      <Text
+        buffer={buffer}
+        mode="text/x-csrc"
+        theme={theme}
+        fontSize={fontSize}
+        lineWrapping={wrapText}
+        selection={theMarker}
+        lineNumbers={!!theFunction}
+        readOnly
+        styleActiveLine={!!theFunction}
+        extraKeys={{ 'Alt-F': 'findPersistent' }}
+      />
+    </>
+  );
+
+};
+
+// --------------------------------------------------------------------------
+// --- Export Component
+// --------------------------------------------------------------------------
+
+export default () => (
+  <Component
+    id="frama-c.sourcecode"
+    label="Source Code"
+    title="Original source code"
+  >
+    <SourceCode />
+  </Component>
+);
+
+// --------------------------------------------------------------------------
-- 
GitLab