diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx index aaf770e7399bc4c1bb8c4709fe7982c573ad930f..d34b86c604720ca4c258205a13c62c0c7e4bb7c4 100644 --- a/ivette/src/frama-c/kernel/SourceCode.tsx +++ b/ivette/src/frama-c/kernel/SourceCode.tsx @@ -28,7 +28,7 @@ import React from 'react'; import * as States from 'frama-c/states'; import * as Dome from 'dome'; -import { readFile } from 'dome/system'; +import * as System from 'dome/system'; import { RichTextBuffer } from 'dome/text/buffers'; import { Text } from 'dome/text/editors'; import { TitleBar } from 'ivette'; @@ -36,10 +36,15 @@ import * as Preferences from 'ivette/prefs'; import { functions, markerInfo } from 'frama-c/api/kernel/ast'; import { Code } from 'dome/controls/labels'; import { Hfill } from 'dome/layout/boxes'; +import { IconButton } from 'dome/controls/buttons'; import * as Path from 'path'; +import * as Settings from 'dome/data/settings'; +import * as Status from 'frama-c/kernel/Status'; +import CodeMirror from 'codemirror/lib/codemirror'; import 'codemirror/addon/selection/active-line'; import 'codemirror/addon/dialog/dialog.css'; +import 'codemirror/addon/search/search'; import 'codemirror/addon/search/searchcursor'; // -------------------------------------------------------------------------- @@ -86,17 +91,64 @@ export default function SourceCode() { // 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 read = () => System.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]); + /* CodeMirror types used to bind callbacks to extraKeys. */ + type position = CodeMirror.Position; + type editor = CodeMirror.Editor; + + const [command] = Settings.useGlobalSettings(Preferences.EditorCommand); + async function launchEditor(_?: editor, pos?: position) { + if (file !== '') { + const selectedLine = pos ? (pos.line + 1).toString() : '1'; + const selectedChar = pos ? (pos.ch + 1).toString() : '1'; + const cmd = command + .replace('%s', file) + .replace('%n', selectedLine) + .replace('%c', selectedChar); + const args = cmd.split(' '); + const prog = args.shift(); + if (prog) System.spawn(prog, args).catch(() => { + Status.setMessage({ + text: `An error has occured when opening the external editor ${prog}`, + kind: 'error', + }); + }); + } + } + + async function contextMenu(editor?: editor, pos?: position) { + if (file !== '') { + const items = [ + { + label: 'Open file in an external editor', + onClick: () => launchEditor(editor, pos), + }, + ]; + Dome.popupMenu(items); + } + } + + const externalEditorTitle = + 'Open the source file in an external editor.\nA Ctrl-click ' + + 'in the source code opens the editor at the selected location.' + + '\nThe editor used can be configured in Ivette settings.'; + // Building the React component. return ( <> <TitleBar> - <Code title={file}>{filename}</Code> + <IconButton + icon="DUPLICATE" + visible={file !== ''} + onClick={launchEditor} + title={externalEditorTitle} + /> + <Code title={file} style={{ padding: '5px' }}>{filename}</Code> <Hfill /> {themeButtons} </TitleBar> @@ -109,7 +161,11 @@ export default function SourceCode() { selection={theMarker} lineNumbers={!!theFunction} styleActiveLine={!!theFunction} - extraKeys={{ 'Alt-F': 'findPersistent' }} + extraKeys={{ + 'Alt-F': 'findPersistent', + 'Ctrl-LeftClick': launchEditor as (_: CodeMirror.Editor) => void, + RightClick: contextMenu as (_: CodeMirror.Editor) => void, + }} readOnly /> </> diff --git a/ivette/src/ivette/prefs.tsx b/ivette/src/ivette/prefs.tsx index 71df4d94ca47ab24bd17921320064355fec3cafe..645e90c1b2a30eff502430aa4fd9c3f48b4adaa0 100644 --- a/ivette/src/ivette/prefs.tsx +++ b/ivette/src/ivette/prefs.tsx @@ -127,3 +127,14 @@ export function useThemeButtons(props: ThemeProps): ThemeControls { } // -------------------------------------------------------------------------- +// --- Editor configuration +// -------------------------------------------------------------------------- + +export const EditorCommand = + new Settings.GString('Editor.Command', 'emacs +%n:%c %s'); + +export interface EditorCommandProps { + command: Settings.GlobalSettings<string>; +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/renderer/Preferences.tsx b/ivette/src/renderer/Preferences.tsx index a16aea3fdb2d8aa71fef5f9ed7451e9a832b52f4..023d17b672a4b63984b193bdc70b501e6443c833 100644 --- a/ivette/src/renderer/Preferences.tsx +++ b/ivette/src/renderer/Preferences.tsx @@ -83,6 +83,20 @@ function ThemeFields(props: P.ThemeProps) { ); } +// -------------------------------------------------------------------------- +// --- Editor Command Forms +// -------------------------------------------------------------------------- +function EditorCommandFields(props: P.EditorCommandProps) { + const cmd = Forms.useDefined(Forms.useValid( + Settings.useGlobalSettings(props.command), + )); + const title = + 'Command to open an external editor on Ctrl-click in the source code view.' + + '\nUse %s for the file name, %n for the line number' + + ' and %c for the selected character.'; + return (<Forms.TextCodeField state={cmd} label="Command" title={title} />); +} + // -------------------------------------------------------------------------- // --- Export Components // -------------------------------------------------------------------------- @@ -105,6 +119,9 @@ export default (() => ( wrapText={P.SourceWrapText} /> </Forms.Section> + <Forms.Section label="Editor Command" unfold> + <EditorCommandFields command={P.EditorCommand} /> + </Forms.Section> </Forms.Page> ));