diff --git a/ivette/src/dome/renderer/dome.tsx b/ivette/src/dome/renderer/dome.tsx index f09729d827c30505a63dab087f11065d71d79d07..0edda413386105a2b24ff69313cb8dc09149229b 100644 --- a/ivette/src/dome/renderer/dome.tsx +++ b/ivette/src/dome/renderer/dome.tsx @@ -518,6 +518,29 @@ export function useUpdate(...events: Event<any>[]) { }); } +/** + Hook to re-render when a Promise returns. + The promise will be typically created by using `React.useMemo()`. + The hook returns three informations: + - result: the promise result if it succeeds, undefined otherwise; + - error: the promise error if it fails, undefined otherwise; + - loading: the promise status, true if the promise is still running. +*/ +export function usePromise<T>(job: Promise<T>) { + const [result, setResult] = React.useState<T | undefined>(); + const [error, setError] = React.useState<Error | undefined>(); + const [loading, setLoading] = React.useState(true); + React.useEffect(() => { + let cancel = false; + const doCancel = () => { if (!cancel) setLoading(false); return cancel; }; + const onResult = (x: T) => { if (!doCancel()) setResult(x); }; + const onError = (e: Error) => { if (!doCancel()) setError(e); }; + job.then(onResult, onError); + return () => { cancel = true; }; + }, [job]); + return { result, error, loading }; +} + // -------------------------------------------------------------------------- // --- Timer Hooks // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/renderer/text/buffers.ts b/ivette/src/dome/renderer/text/buffers.ts index 02dd4616f8da30a5666b904e008fbfea43b7779e..712089fa924a872903f7903c867cd41b43dfdecd 100644 --- a/ivette/src/dome/renderer/text/buffers.ts +++ b/ivette/src/dome/renderer/text/buffers.ts @@ -478,6 +478,19 @@ export class RichTextBuffer extends Emitter { } } + /** + Requires all connected views to select the specified line and to place it + at the top of their displays. + + @param line - the line number to select + */ + setCursorOnTop(line: number): void { + this.forEach((cm) => { + cm.setCursor(cm.lastLine()); + cm.setCursor(line - 1); + }); + } + // -------------------------------------------------------------------------- // --- Document Linking // -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx index b635b6facafb4e03e68f036e3b61d139dbe154bc..aaf770e7399bc4c1bb8c4709fe7982c573ad930f 100644 --- a/ivette/src/frama-c/kernel/SourceCode.tsx +++ b/ivette/src/frama-c/kernel/SourceCode.tsx @@ -33,14 +33,14 @@ import { RichTextBuffer } from 'dome/text/buffers'; import { Text } from 'dome/text/editors'; import { TitleBar } from 'ivette'; import * as Preferences from 'ivette/prefs'; -import { functions, markerInfo, source } from 'frama-c/api/kernel/ast'; +import { functions, markerInfo } from 'frama-c/api/kernel/ast'; +import { Code } from 'dome/controls/labels'; +import { Hfill } from 'dome/layout/boxes'; +import * as Path from 'path'; 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'; // -------------------------------------------------------------------------- // --- Pretty Printing (Browser Console) @@ -52,13 +52,28 @@ const D = new Dome.Debug('Source Code'); // --- Source Code Printer // -------------------------------------------------------------------------- +// The SourceCode component, producing the GUI part showing the source code +// corresponding to the selected function. export default function SourceCode() { // Hooks - const buffer = React.useMemo(() => new RichTextBuffer(), []); + const [buffer] = React.useState(new RichTextBuffer()); const [selection] = States.useSelection(); const theFunction = selection?.current?.fct; const theMarker = selection?.current?.marker; + const markersInfo = States.useSyncArray(markerInfo); + const functionsData = States.useSyncArray(functions).getArray(); + + // Retrieving the file name and the line number from the selection and the + // synchronized tables. + const sloc = + (theMarker && markersInfo.getData(theMarker)?.sloc) ?? + (theFunction && functionsData.find((e) => e.name === theFunction)?.sloc); + const file = sloc ? sloc.file : ''; + const line = sloc ? sloc.line : 0; + const filename = Path.parse(file).base; + + // Title bar buttons, along with the parameters for our text. const { buttons: themeButtons, theme, fontSize, wrapText } = Preferences.useThemeButtons({ target: 'Source Code', @@ -68,46 +83,21 @@ export default function SourceCode() { disabled: !theFunction, }); - const markersInfo = States.useSyncArray(markerInfo); - const functionsData = States.useSyncArray(functions).getArray(); - - const currentFile = React.useRef<string>(); - - React.useEffect(() => { - // Async source file loading and jump to line/location. - async function loadSourceCode(sloc?: source) { - if (sloc) { - const { file, line } = sloc; - try { - if (file !== currentFile.current) { - currentFile.current = file; - const content = await readFile(file); - buffer.setValue(content); - } - buffer.forEach((cm) => { cm.setCursor(line - 1); }); - } catch (err) { - D.error(`Fail to load source code file ${file}.`); - } - } - } - // Actual source code loading upon function or marker update. - const sloc = - /* markers have more precise source location */ - (theMarker && markersInfo.getData(theMarker)?.sloc) - ?? - (theFunction && functionsData.find((e) => e.name === theFunction)?.sloc); - if (sloc) { - loadSourceCode(sloc); - } else { - currentFile.current = undefined; - buffer.clear(); - } - }, [buffer, functionsData, markersInfo, theFunction, theMarker]); + // Updating the buffer content. + const errorMsg = () => { D.error(`Fail to load source code file ${file}`); }; + const onError = () => { if (file) errorMsg(); return ''; }; + const read = () => readFile(file).catch(onError); + const text = React.useMemo(read, [file, onError]); + const { result } = Dome.usePromise(text); + React.useEffect(() => buffer.setValue(result), [buffer, result]); + React.useEffect(() => buffer.setCursorOnTop(line), [buffer, line, result]); - // Component + // Building the React component. return ( <> <TitleBar> + <Code title={file}>{filename}</Code> + <Hfill /> {themeButtons} </TitleBar> <Text