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