diff --git a/ivette/.eslintignore b/ivette/.eslintignore index ba02ae7397f98663969ed6a85953c7f336d21644..8744f886e55e783cec65c67ed1630b8bb0bc08c4 100644 --- a/ivette/.eslintignore +++ b/ivette/.eslintignore @@ -8,3 +8,6 @@ coverage lib # don't lint the generated API api + +# ZMQ Server is not working (yet) +src/frama-c/client_zmq.ts diff --git a/ivette/.eslintrc.js b/ivette/.eslintrc.js index 322375d876f3c51d95ac3358b423fc19b4f04e30..5a3a472d6a2dddb2ad443fbe21354075b6731341 100644 --- a/ivette/.eslintrc.js +++ b/ivette/.eslintrc.js @@ -3,6 +3,7 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', + 'import', ], extends: [ 'airbnb-typescript', @@ -32,15 +33,13 @@ module.exports = { "@typescript-eslint/no-explicit-any": "off", // Allow functions without return type, even if exported function should have one "@typescript-eslint/explicit-function-return-type": "off", - // Allow function hoisting, even if it should be avoided - "no-use-before-define": [ - "error", - { functions: false, classes: true, variables: true }, - ], - "@typescript-eslint/no-use-before-define": [ - "error", - { functions: false, classes: true, variables: true, typedefs: true }, - ], + // Allow function hoisting, even if it should be avoided" + "@typescript-eslint/lines-between-class-members": "off", + "@typescript-eslint/space-before-function-paren": "off", + "@typescript-eslint/naming-convention": "off", + "no-constant-condition": ["error", { "checkLoops": false }], + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "off", // Prefer const when _all_ destructured values may be const "prefer-const": [ "error", @@ -51,7 +50,7 @@ module.exports = { // Allow return statements even if not strictly needed "no-useless-return": "off", // Forbid shadowing concerning variables - "no-shadow": "error", + "no-shadow": "off", // Force single class member per line "lines-between-class-members": [ "error", "always", { "exceptAfterSingleLine": true } diff --git a/ivette/src/dome/main/dome.ts b/ivette/src/dome/main/dome.ts index 605cfbd5d84d6ec0fdfe602692e0e1221c4acba6..98bdd45a4776033eb6b088758adb8d051377fe00 100644 --- a/ivette/src/dome/main/dome.ts +++ b/ivette/src/dome/main/dome.ts @@ -174,9 +174,9 @@ ipcMain.on('dome.ipc.settings.sync', windowSyncSettings); // --- Patching Settings // -------------------------------------------------------------------------- -type patch = { key: string; value: any }; +type Patch = { key: string; value: any }; -function applyPatches(data: Store, args: patch[]) { +function applyPatches(data: Store, args: Patch[]) { args.forEach(({ key, value }) => { if (value === null) { delete data[key]; @@ -186,7 +186,7 @@ function applyPatches(data: Store, args: patch[]) { }); } -function applyWindowSettings(event: IpcMainEvent, args: patch[]) { +function applyWindowSettings(event: IpcMainEvent, args: Patch[]) { const handle = WindowHandles.get(event.sender.id); if (handle) { applyPatches(handle.settings, args); @@ -194,7 +194,7 @@ function applyWindowSettings(event: IpcMainEvent, args: patch[]) { } } -function applyStorageSettings(event: IpcMainEvent, args: patch[]) { +function applyStorageSettings(event: IpcMainEvent, args: Patch[]) { const handle = WindowHandles.get(event.sender.id); if (handle) { applyPatches(handle.storage, args); @@ -202,7 +202,7 @@ function applyStorageSettings(event: IpcMainEvent, args: patch[]) { } } -function applyGlobalSettings(event: IpcMainEvent, args: patch[]) { +function applyGlobalSettings(event: IpcMainEvent, args: Patch[]) { applyPatches(obtainGlobalSettings(), args); BrowserWindow.getAllWindows().forEach((w: BrowserWindow) => { const contents = w.webContents; @@ -274,7 +274,7 @@ function getURL() { return `file://${__dirname}/index.html`; } -function navigateURL(sender: Electron.webContents) { +function navigateURL(sender: Electron.WebContents) { return (event: Electron.Event, url: string) => { event.preventDefault(); const href = new URL(url); diff --git a/ivette/src/dome/main/menubar.ts b/ivette/src/dome/main/menubar.ts index d22df5c443ed5515b682d289ec1ff83d01413fcb..21cdb1ee679029ef96dfaa949189e35ffbf1dd3b 100644 --- a/ivette/src/dome/main/menubar.ts +++ b/ivette/src/dome/main/menubar.ts @@ -25,9 +25,8 @@ // -------------------------------------------------------------------------- /* eslint-disable max-len */ -/* eslint-disable @typescript-eslint/camelcase */ -import { app, ipcMain, BrowserWindow, Menu, MenuItem, shell } from 'electron'; +import { app, ipcMain, BrowserWindow, Menu, MenuItem, shell, KeyboardEvent } from 'electron'; import * as System from 'dome/system'; // -------------------------------------------------------------------------- @@ -48,16 +47,33 @@ function reloadWindow() { }); } -function toggleFullScreen(_item: MenuItem, focusedWindow: BrowserWindow) { +function toggleFullScreen( + _item: MenuItem, + focusedWindow: BrowserWindow | undefined, + _evt: KeyboardEvent, +) { if (focusedWindow) focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); } -function toggleDevTools(_item: MenuItem, focusedWindow: BrowserWindow) { +function toggleDevTools( + _item: MenuItem, + focusedWindow: BrowserWindow | undefined, + _evt: KeyboardEvent, +) { if (focusedWindow) focusedWindow.webContents.toggleDevTools(); } +function userFindInfo( + _item: MenuItem, + focusedWindow: BrowserWindow | undefined, + _evt: KeyboardEvent, +) { + if (focusedWindow) + focusedWindow.webContents.send('dome.ipc.find'); +} + // -------------------------------------------------------------------------- // --- Menu Utilities // -------------------------------------------------------------------------- @@ -131,9 +147,9 @@ const macosAppMenuItems = (appName: string): MenuSpec => [ // --- File Menu Items (platform dependant) // -------------------------------------------------------------------------- -const fileMenuItems_custom: MenuSpec = []; +const fileMenuItemsCustom: MenuSpec = []; -const fileMenuItems_linux: MenuSpec = [ +const fileMenuItemsLinux: MenuSpec = [ { label: 'Preferences…', click: () => ipcMain.emit('dome.menu.settings'), @@ -154,7 +170,7 @@ const fileMenuItems_linux: MenuSpec = [ // --- Edit Menu Items // -------------------------------------------------------------------------- -const editMenuItems_custom: MenuSpec = []; +const editMenuItemsCustom: MenuSpec = []; const editMenuItems: MenuSpec = [ { @@ -188,11 +204,7 @@ const editMenuItems: MenuSpec = [ { label: 'Find', accelerator: 'CmdOrCtrl+F', - click: ( - _item: Electron.MenuItem, - window: Electron.BrowserWindow, - _evt: Electron.KeyboardEvent, - ) => window.webContents.send('dome.ipc.find'), + click: userFindInfo, }, ]; @@ -200,7 +212,7 @@ const editMenuItems: MenuSpec = [ // --- View Menu Items // -------------------------------------------------------------------------- -const viewMenuItems_custom: MenuSpec = []; +const viewMenuItemsCustom: MenuSpec = []; const viewMenuItems = (osx: boolean): MenuSpec => [ { @@ -222,7 +234,7 @@ const viewMenuItems = (osx: boolean): MenuSpec => [ // --- Window Menu Items // -------------------------------------------------------------------------- -const windowMenuItems_linux: MenuSpec = [ +const windowMenuItemsLinux: MenuSpec = [ { label: 'Minimize', accelerator: 'CmdOrCtrl+M', @@ -241,7 +253,7 @@ const windowMenuItems_linux: MenuSpec = [ }, ]; -const windowMenuItems_macos: MenuSpec = windowMenuItems_linux.concat([ +const windowMenuItemsMacos: MenuSpec = windowMenuItemsLinux.concat([ { label: 'Bring All to Front', role: 'front', @@ -292,9 +304,9 @@ const customItems = new Map<string, ItemEntry>(); function findMenu(label: string): MenuSpec | undefined { switch (label) { - case 'File': return fileMenuItems_custom; - case 'Edit': return editMenuItems_custom; - case 'View': return viewMenuItems_custom; + case 'File': return fileMenuItemsCustom; + case 'Edit': return editMenuItemsCustom; + case 'View': return viewMenuItemsCustom; default: { const cm = customMenus.find((m) => m.label === label); return cm && cm.submenu; @@ -317,12 +329,12 @@ export interface CustomMenuItem extends MenuItemSpec { key?: string; } -export interface Separator { +export interface SeparatorItem { menu: string; type: 'separator'; } -export type CustomMenuItemSpec = Separator | CustomMenuItem; +export type CustomMenuItemSpec = SeparatorItem | CustomMenuItem; export function addMenuItem(custom: CustomMenuItemSpec) { const menuSpec = findMenu(custom.menu); @@ -361,10 +373,10 @@ export function addMenuItem(custom: CustomMenuItemSpec) { } else { if (!spec.click && !spec.role) spec.click = ( - _item: Electron.MenuItem, - window: Electron.BrowserWindow, - _evt: Electron.KeyboardEvent, - ) => window.webContents.send('dome.ipc.menu.clicked', id); + _item: MenuItem, + window: BrowserWindow | undefined, + _evt: KeyboardEvent, + ) => window?.webContents.send('dome.ipc.menu.clicked', id); customItems.set(id, { spec }); menuSpec.push(spec); } @@ -392,13 +404,13 @@ function template(): CustomMenu[] { return ([] as CustomMenu[]).concat( [ { label: app.name, submenu: macosAppMenuItems(app.name) }, - { label: 'File', submenu: fileMenuItems_custom }, - { label: 'Edit', submenu: concatSep(editMenuItems, editMenuItems_custom) }, - { label: 'View', submenu: concatSep(viewMenuItems_custom, viewMenuItems(true)) }, + { label: 'File', submenu: fileMenuItemsCustom }, + { label: 'Edit', submenu: concatSep(editMenuItems, editMenuItemsCustom) }, + { label: 'View', submenu: concatSep(viewMenuItemsCustom, viewMenuItems(true)) }, ], customMenus, [ - { label: 'Window', role: 'window', submenu: windowMenuItems_macos }, + { label: 'Window', role: 'window', submenu: windowMenuItemsMacos }, { label: 'Help', role: 'help', submenu: helpMenuItems }, ], ); @@ -407,13 +419,13 @@ function template(): CustomMenu[] { default: return ([] as CustomMenu[]).concat( [ - { label: 'File', submenu: concatSep(fileMenuItems_custom, fileMenuItems_linux) }, - { label: 'Edit', submenu: concatSep(editMenuItems, editMenuItems_custom) }, - { label: 'View', submenu: concatSep(viewMenuItems_custom, viewMenuItems(false)) }, + { label: 'File', submenu: concatSep(fileMenuItemsCustom, fileMenuItemsLinux) }, + { label: 'Edit', submenu: concatSep(editMenuItems, editMenuItemsCustom) }, + { label: 'View', submenu: concatSep(viewMenuItemsCustom, viewMenuItems(false)) }, ], customMenus, [ - { label: 'Window', submenu: windowMenuItems_linux }, + { label: 'Window', submenu: windowMenuItemsLinux }, { label: 'Help', submenu: helpMenuItems }, ], ); @@ -445,9 +457,9 @@ export function install() { // Called by reload above function reset() { - fileMenuItems_custom.length = 0; - editMenuItems_custom.length = 0; - viewMenuItems_custom.length = 0; + fileMenuItemsCustom.length = 0; + editMenuItemsCustom.length = 0; + viewMenuItemsCustom.length = 0; customMenus.length = 0; customItems.clear(); install(); diff --git a/ivette/src/dome/misc/system.ts b/ivette/src/dome/misc/system.ts index 99bcf87c156d06697c1cf614b40227f90fc06f62..df85d83050c5c70b22c7707c5c2d48cd6dc6a0d6 100644 --- a/ivette/src/dome/misc/system.ts +++ b/ivette/src/dome/misc/system.ts @@ -36,9 +36,9 @@ import Emitter from 'events'; import Exec from 'child_process'; import fspath from 'path'; import fs from 'fs'; -import { app, remote } from 'electron'; +import { app } from 'electron'; -declare const __static: string; +declare const staticPath: string; // -------------------------------------------------------------------------- // --- Platform Specificities @@ -121,7 +121,7 @@ export function doExit() { let COMMAND_WDIR = '.'; let COMMAND_ARGV: string[] = []; -function SET_COMMAND(argv: string[], wdir: string) { +function setCommandLine(argv: string[], wdir: string) { COMMAND_ARGV = argv; COMMAND_WDIR = wdir; } @@ -130,22 +130,20 @@ function SET_COMMAND(argv: string[], wdir: string) { // --- User's Directories // -------------------------------------------------------------------------- -const appProxy = app || remote.app; - /** Returns user's home directory. */ -export function getHome() { return appProxy.getPath('home'); } +export function getHome() { return app.getPath('home'); } /** Returns user's desktop directory. */ -export function getDesktop() { return appProxy.getPath('desktop'); } +export function getDesktop() { return app.getPath('desktop'); } /** Returns user's documents directory. */ -export function getDocuments() { return appProxy.getPath('documents'); } +export function getDocuments() { return app.getPath('documents'); } /** Returns user's downloads directory. */ -export function getDownloads() { return appProxy.getPath('downloads'); } +export function getDownloads() { return app.getPath('downloads'); } /** Returns temporary directory. */ -export function getTempDir() { return appProxy.getPath('temp'); } +export function getTempDir() { return app.getPath('temp'); } /** Working directory (Application Window). @@ -185,7 +183,7 @@ export function getArguments() { return COMMAND_ARGV; } configuration. */ export function getStatic(...path: string[]) { - return fspath.join(__static, ...path); + return fspath.join(staticPath, ...path); } // -------------------------------------------------------------------------- @@ -308,14 +306,10 @@ export function exists(path: string) { /** Reads a textual file contents. - Promisified - [Node `fs.readFile`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_readfile_path_options_callback) - using `UTF-8` encoding. + Promisified `fs.readFile` using `utf-8` encoding. */ export function readFile(path: string): Promise<string> { - return new Promise((result, reject) => { - fs.readFile(path, 'UTF-8', (err, data) => (err ? reject(err) : result(data))); - }); + return fs.promises.readFile(path, { encoding: 'utf-8' }); } // -------------------------------------------------------------------------- @@ -325,14 +319,10 @@ export function readFile(path: string): Promise<string> { /** Writes a textual content in a file. - Promisified - [Node `fs.writeFile`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_writefile_file_data_options_callback) - using `UTF-8` encoding. + Promisified `fs.writeFile` using `utf-8` encoding. */ -export function writeFile(path: string, content: string): Promise<void> { - return new Promise((result, reject) => { - fs.writeFile(path, content, 'UTF-8', (err) => (err ? reject(err) : result())); - }); +export async function writeFile(path: string, content: string): Promise<void> { + return fs.promises.writeFile(path, content, { encoding: 'utf-8' }); } // -------------------------------------------------------------------------- @@ -344,14 +334,10 @@ export function writeFile(path: string, content: string): Promise<void> { @param srcPath - the source file path @param tgtPath - the target file path - Promisified - [Node `fs.copyFile`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_copyfile_src_dest_flags_callback) - using `UTF-8` encoding. + Promisified `fs.copyFile`. */ -export function copyFile(srcPath: string, tgtPath: string): Promise<void> { - return new Promise((result, reject) => { - fs.copyFile(srcPath, tgtPath, (err) => (err ? reject(err) : result())); - }); +export async function copyFile(srcPath: string, tgtPath: string): Promise<void> { + return fs.promises.copyFile(srcPath, tgtPath); } // -------------------------------------------------------------------------- @@ -362,24 +348,15 @@ export function copyFile(srcPath: string, tgtPath: string): Promise<void> { Reads a directory. @returns directory contents (local names) - Promisified - [Node `fs.readdir`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_readdir_path_options_callback). + Promisified `fs.readdir`. - Uses `UTF-8` encoding to obtain (relative) file names instead of byte buffers. On MacOS, `.DS_Store` entries - are filtered out. + Uses `utf-8` encoding to obtain (relative) file names instead of byte buffers. + On MacOS, `.DS_Store` entries are filtered out. */ -export function readDir(path: string): Promise<string[]> { +export async function readDir(path: string): Promise<string[]> { const filterDir = (f: string) => f !== '.DS_Store'; - return new Promise((result, reject) => { - fs.readdir( - path, - { encoding: 'UTF-8', withFileTypes: true }, - (err: NodeJS.ErrnoException | null, files: fs.Dirent[]) => { - if (err) reject(err); - else result(files.map((fn) => fn.name).filter(filterDir)); - }, - ); - }); + const entries = await fs.promises.readdir(path, { encoding: 'utf-8', withFileTypes: true }); + return entries.map((fn) => fn.name).filter(filterDir); } // -------------------------------------------------------------------------- @@ -441,7 +418,8 @@ async function rmDirRec(path: string): Promise<void> { try { const stats = fs.statSync(path); if (stats.isFile()) { - return remove(path); + await remove(path); + return; } if (stats.isDirectory()) { const rmDirSub = (name: string) => { @@ -506,9 +484,9 @@ atExit(() => { export type StdPipe = { path?: string | undefined; mode?: number; pipe?: boolean }; export type StdOptions = undefined | 'null' | 'ignore' | 'pipe' | StdPipe; -type stdio = { io: number | 'pipe' | 'ignore' | 'ipc'; fd?: number }; +type StdIO = { io: number | 'pipe' | 'ignore' | 'ipc'; fd?: number }; -function stdSpec(spec: StdOptions, isOutput: boolean): stdio { +function stdSpec(spec: StdOptions, isOutput: boolean): StdIO { switch (spec) { case undefined: return { io: isOutput ? 'pipe' : 'ignore' }; @@ -533,7 +511,7 @@ interface Readable { function pipeTee(std: Readable, fd: number) { if (!fd) return; - const out = fs.createWriteStream('<ignored>', { fd, encoding: 'UTF-8' }); + const out = fs.createWriteStream('<ignored>', { fd, encoding: 'utf-8' }); out.on('error', (err) => { console.warn('[Dome] can not pipe:', err); std.unpipe(out); @@ -627,11 +605,11 @@ export function spawn( const err = child.stderr; if (out && stdout.fd) { - out.setEncoding('UTF-8'); + out.setEncoding('utf-8'); pipeTee(out, stdout.fd); } if (err && stderr.fd) { - err.setEncoding('UTF-8'); + err.setEncoding('utf-8'); pipeTee(err, stderr.fd); } @@ -651,7 +629,7 @@ const WINDOW_PREFERENCES_ARGV = '--dome-preferences-window'; // -------------------------------------------------------------------------- export default { - SET_COMMAND, + setCommandLine, WINDOW_APPLICATION_ARGV, WINDOW_PREFERENCES_ARGV, }; diff --git a/ivette/src/dome/misc/utils.ts b/ivette/src/dome/misc/utils.ts index b15ff61229e284e6c022a5eea04f439168e90176..2d896660ba950b8c98dc056b8910638dea42ab12 100644 --- a/ivette/src/dome/misc/utils.ts +++ b/ivette/src/dome/misc/utils.ts @@ -31,9 +31,9 @@ import type { CSSProperties } from 'react'; -type falsy = undefined | boolean | null | ''; +type Falsy = undefined | boolean | null | ''; -export type ClassSpec = string | falsy | { [cname: string]: true | falsy }; +export type ClassSpec = string | Falsy | { [cname: string]: true | Falsy }; /** Utility function to merge various HTML class properties @@ -75,7 +75,7 @@ export function classes( return buffer.join(' '); } -export type StyleSpec = falsy | CSSProperties; +export type StyleSpec = Falsy | CSSProperties; /** Utility function to merge various CSS style properties diff --git a/ivette/src/dome/renderer/data/compare.ts b/ivette/src/dome/renderer/data/compare.ts index 26dc17956720dfca5419f5034286885d1f83baa3..5254f927550462a91c12d75b8b62874f67756b57 100644 --- a/ivette/src/dome/renderer/data/compare.ts +++ b/ivette/src/dome/renderer/data/compare.ts @@ -56,10 +56,10 @@ export const isEqual = FastCompare; export function equal(_x: any, _y: any): 0 { return 0; } /** Primitive comparison works on this type. */ -export type bignum = bigint | number; +export type BigNum = bigint | number; /** Detect Non-NaN numbers and big-ints. */ -export function isBigNum(x: any): x is bignum { +export function isBigNum(x: any): x is BigNum { return ( (typeof (x) === 'bigint') || (typeof (x) === 'number' && !Number.isNaN(x)) @@ -91,7 +91,7 @@ export const string: Order<string> = primitive; /** Primitive comparison for (big) integers (non NaN numbers included). */ -export const bignum: Order<bignum> = primitive; +export const bignum: Order<BigNum> = primitive; /** Primitive comparison for number (NaN included). @@ -291,13 +291,13 @@ export function byAllFields<A>(order: ByAllFields<A>): Order<A> { }; } -export type dict<A> = undefined | null | { [key: string]: A }; +export type Dict<A> = undefined | null | { [key: string]: A }; /** Compare dictionaries _wrt_ lexicographic order of entries. */ -export function dictionary<A>(order: Order<A>): Order<dict<A>> { - return (x: dict<A>, y: dict<A>) => { +export function dictionary<A>(order: Order<A>): Order<Dict<A>> { + return (x: Dict<A>, y: Dict<A>) => { if (x === y) return 0; const dx = x ?? {}; const dy = y ?? {}; @@ -398,29 +398,31 @@ export function tuple5<A, B, C, D, E>( // --- Structural Comparison // -------------------------------------------------------------------------- +/* eslint-disable no-shadow */ + /** @internal */ -enum RANK { +enum IRANK { UNDEFINED, BOOLEAN, SYMBOL, NAN, BIGNUM, STRING, - ARRAY, OBJECT, FUNCTION + ARRAY, OBJECT, FUNCTION, } /** @internal */ -function rank(x: any): RANK { +function rank(x: any): IRANK { const t = typeof x; switch (t) { - case 'undefined': return RANK.UNDEFINED; - case 'boolean': return RANK.BOOLEAN; - case 'symbol': return RANK.SYMBOL; + case 'undefined': return IRANK.UNDEFINED; + case 'boolean': return IRANK.BOOLEAN; + case 'symbol': return IRANK.SYMBOL; case 'number': - return Number.isNaN(x) ? RANK.NAN : RANK.BIGNUM; + return Number.isNaN(x) ? IRANK.NAN : IRANK.BIGNUM; case 'bigint': - return RANK.BIGNUM; - case 'string': return RANK.STRING; - case 'function': return RANK.FUNCTION; + return IRANK.BIGNUM; + case 'string': return IRANK.STRING; + case 'function': return IRANK.FUNCTION; case 'object': - return Array.isArray(x) ? RANK.ARRAY : RANK.OBJECT; + return Array.isArray(x) ? IRANK.ARRAY : IRANK.OBJECT; } } diff --git a/ivette/src/dome/renderer/data/json.ts b/ivette/src/dome/renderer/data/json.ts index b848fc1671087713f5c981e4b718cf85a3012a64..1d0a52ad8928d2ba45d11b8d1259e2532fed246a 100644 --- a/ivette/src/dome/renderer/data/json.ts +++ b/ivette/src/dome/renderer/data/json.ts @@ -32,6 +32,8 @@ import { DEVEL } from 'dome/system'; +/* eslint-disable @typescript-eslint/naming-convention, no-shadow */ + export type json = undefined | null | boolean | number | string | json[] | { [key: string]: json }; diff --git a/ivette/src/dome/renderer/dialogs.tsx b/ivette/src/dome/renderer/dialogs.tsx index bbdda3f02693deb736dca5a2a21142fb060662a7..d0936827972eeca84acf34c07efcaf4593630eb2 100644 --- a/ivette/src/dome/renderer/dialogs.tsx +++ b/ivette/src/dome/renderer/dialogs.tsx @@ -27,7 +27,7 @@ */ import filepath from 'path'; -import { remote } from 'electron'; +import { dialog } from 'electron'; import * as System from 'dome/system'; // -------------------------------------------------------------------------- @@ -122,8 +122,7 @@ export async function showMessageBox<A>( if (cancelId === defaultId) cancelId = -1; - return remote.dialog.showMessageBox( - remote.getCurrentWindow(), + return dialog.showMessageBox( { type: kind, message, @@ -197,8 +196,7 @@ export async function showOpenFile( props: OpenFileProps, ): Promise<string | undefined> { const { title, label, path, hidden = false, filters } = props; - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), + return dialog.showOpenDialog( { title, buttonLabel: label, @@ -221,8 +219,7 @@ export async function showOpenFiles( ): Promise<string[] | undefined> { const { title, label, path, hidden, filters } = props; - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), + return dialog.showOpenDialog( { title, buttonLabel: label, @@ -258,8 +255,7 @@ export async function showSaveFile( props: SaveFileProps, ): Promise<string | undefined> { const { title, label, path, filters } = props; - return remote.dialog.showSaveDialog( - remote.getCurrentWindow(), + return dialog.showSaveDialog( { title, buttonLabel: label, @@ -292,8 +288,7 @@ export async function showOpenDir( default: break; } - return remote.dialog.showOpenDialog( - remote.getCurrentWindow(), + return dialog.showOpenDialog( { title, buttonLabel: label, diff --git a/ivette/src/dome/renderer/dome.tsx b/ivette/src/dome/renderer/dome.tsx index 0edda413386105a2b24ff69313cb8dc09149229b..63386e2fb374fe922ad4098cd04866443be27a9e 100644 --- a/ivette/src/dome/renderer/dome.tsx +++ b/ivette/src/dome/renderer/dome.tsx @@ -42,7 +42,7 @@ import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; import { AppContainer } from 'react-hot-loader'; -import { remote, ipcRenderer } from 'electron'; +import { Menu, MenuItem, ipcRenderer } from 'electron'; import SYS, * as System from 'dome/system'; import * as Json from 'dome/data/json'; import * as Settings from 'dome/data/settings'; @@ -168,7 +168,7 @@ export function onCommand( } ipcRenderer.on('dome.ipc.command', (_event, argv, wdir) => { - SYS.SET_COMMAND(argv, wdir); + SYS.setCommandLine(argv, wdir); System.emitter.emit('dome.command', argv, wdir); }); @@ -467,7 +467,6 @@ export function popupMenu( items: PopupMenuItem[], callback?: (item: string | undefined) => void, ) { - const { Menu, MenuItem } = remote; const menu = new Menu(); let selected = ''; let kid = 0; @@ -489,7 +488,7 @@ export function popupMenu( } }); const job = callback ? () => callback(selected) : undefined; - menu.popup({ window: remote.getCurrentWindow(), callback: job }); + menu.popup({ callback: job }); } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/renderer/errors.tsx b/ivette/src/dome/renderer/errors.tsx index b424ac4ec8832ee2af4999653b2c70f7c83f9bd9..a955124cf33a9dd76ad9d008f1b1e4b27c765555 100644 --- a/ivette/src/dome/renderer/errors.tsx +++ b/ivette/src/dome/renderer/errors.tsx @@ -60,7 +60,7 @@ interface CatchState { /** React Error Boundaries. */ -export class Catch extends React.Component<CatchProps, CatchState, {}> { +export class Catch extends React.Component<CatchProps, CatchState, unknown> { constructor(props: CatchProps) { super(props); diff --git a/ivette/src/dome/renderer/frame/sidebars.tsx b/ivette/src/dome/renderer/frame/sidebars.tsx index 7cbe2a4c7478fd2f3534070287e833d49fff7bc1..ab00240d36aeedf21f565b28fd781f1cb442fe2f 100644 --- a/ivette/src/dome/renderer/frame/sidebars.tsx +++ b/ivette/src/dome/renderer/frame/sidebars.tsx @@ -68,7 +68,7 @@ export function SideBar(props: SideBarProps) { // -------------------------------------------------------------------------- export type BadgeElt = undefined | null | string | number | React.ReactNode; -export type Badge = BadgeElt | BadgeElt[]; +export type Badges = BadgeElt | BadgeElt[]; const makeBadgeElt = (elt: BadgeElt): React.ReactNode => { if (elt === undefined || elt === null) return null; @@ -81,7 +81,7 @@ const makeBadgeElt = (elt: BadgeElt): React.ReactNode => { } }; -const makeBadge = (elt: Badge): React.ReactNode => { +const makeBadge = (elt: Badges): React.ReactNode => { if (Array.isArray(elt)) return elt.map(makeBadgeElt); return makeBadgeElt(elt); @@ -120,7 +120,7 @@ export interface SectionProps { /** Disabled sections are made unvisible. */ disabled?: boolean; /** Badge summary (only visible when folded). */ - summary?: Badge; + summary?: Badges; /** Right-click callback. */ onContextMenu?: () => void; /** Section contents. */ @@ -182,7 +182,7 @@ export interface ItemProps { /** Item tooltip text. */ title?: string; /** Badge. */ - badge?: Badge; + badge?: Badges; /** Enabled item. */ enabled?: boolean; /** Disabled item (dimmed). */ diff --git a/ivette/src/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx index 6064de0c647d07e2fae893a4d307073c0c37c6fc..25c1c067ebd2919c46fd200bbb1ec0e3e5b46c45 100644 --- a/ivette/src/dome/renderer/frame/toolbars.tsx +++ b/ivette/src/dome/renderer/frame/toolbars.tsx @@ -31,7 +31,6 @@ import React from 'react'; import { Event, useEvent, find } from 'dome'; -import { debounce } from 'lodash'; import { SVG } from 'dome/controls/icons'; import { Label } from 'dome/controls/labels'; import { classes } from 'dome/misc/utils'; @@ -95,12 +94,6 @@ const KIND = (kind: undefined | string) => ( kind ? ` dome-xToolBar-${kind}` : '' ); -interface SELECT<A> { - selected?: boolean; - selection?: A; - value?: A; -} - export type ButtonKind = | 'default' | 'cancel' | 'warning' | 'positive' | 'negative'; @@ -268,6 +261,12 @@ export interface SearchFieldProps<A> { event?: null | Event<void>; } +interface Searching { + pattern?: string; + timer?: NodeJS.Timeout | undefined; + onSearch?: ((p: string) => void); +} + /** Search Bar. */ @@ -277,18 +276,24 @@ export function SearchField<A = undefined>(props: SearchFieldProps<A>) { const focus = () => inputRef.current?.focus(); const [value, setValue] = React.useState(''); const [index, setIndex] = React.useState(-1); + const searching = React.useRef<Searching>({}); const { onHint, onSelect, onSearch, hints = [] } = props; // Find event trigger useEvent(props.event ?? find, focus); // Lookup trigger - const triggerLookup = React.useCallback( - debounce((pattern: string) => { - if (onSearch) onSearch(pattern); - }, DEBOUNCED_SEARCH), - [onSearch], - ); + const triggerLookup = React.useCallback((pattern: string) => { + const s = searching.current; + s.pattern = pattern; + s.onSearch = onSearch; + if (!s.timer) { + s.timer = setTimeout(() => { + s.timer = undefined; + if (s.onSearch && s.pattern) s.onSearch(s.pattern); + }, DEBOUNCED_SEARCH); + } + }, [onSearch]); // Blur Event const onBlur = () => { diff --git a/ivette/src/dome/renderer/layout/forms.tsx b/ivette/src/dome/renderer/layout/forms.tsx index 86183586e91b5cbd51902f9cbe389a1bc3430244..79d96f2f1e7291475ba8d34b99ae8c164341df4c 100644 --- a/ivette/src/dome/renderer/layout/forms.tsx +++ b/ivette/src/dome/renderer/layout/forms.tsx @@ -45,12 +45,12 @@ import * as Utils from 'dome/misc/utils'; import { SVG } from 'dome/controls/icons'; import { Checkbox, Radio, Select as SelectMenu } from 'dome/controls/buttons'; -export type Error = +export type FieldError = | undefined | boolean | string - | { [key: string]: Error } | Error[]; -export type Checker<A> = (value: A) => boolean | Error; -export type Callback<A> = (value: A, error: Error) => void; -export type FieldState<A> = [A, Error, Callback<A>]; + | { [key: string]: FieldError } | FieldError[]; +export type Checker<A> = (value: A) => boolean | FieldError; +export type Callback<A> = (value: A, error: FieldError) => void; +export type FieldState<A> = [A, FieldError, Callback<A>]; /* --------------------------------------------------------------------------*/ /* --- State Errors Utilities ---*/ @@ -66,28 +66,28 @@ export function inRange( export function validate<A>( value: A, checker: undefined | Checker<A>, -): Error { +): FieldError { if (checker) { try { const r = checker(value); if (r === undefined || r === true) return undefined; return r; } catch (err) { - return err.toString() || false; + return '' + err || false; } } return undefined; } -export function isValid(err: Error): boolean { return !err; } +export function isValid(err: FieldError): boolean { return !err; } -type ObjectError = { [key: string]: Error }; +type ObjectError = { [key: string]: FieldError }; -function isObjectError(err: Error): err is ObjectError { +function isObjectError(err: FieldError): err is ObjectError { return typeof err === 'object' && !Array.isArray(err); } -function isArrayError(err: Error): err is Error[] { +function isArrayError(err: FieldError): err is FieldError[] { return Array.isArray(err); } @@ -99,7 +99,7 @@ function isValidObject(err: ObjectError): boolean { return true; } -function isValidArray(err: Error[]): boolean { +function isValidArray(err: FieldError[]): boolean { for (let k = 0; k < err.length; k++) { if (!isValid(err[k])) return false; } @@ -117,8 +117,8 @@ export function useState<A>( onChange?: Callback<A>, ): FieldState<A> { const [value, setValue] = React.useState<A>(defaultValue); - const [error, setError] = React.useState<Error>(undefined); - const setState = React.useCallback((newValue: A, newError: Error) => { + const [error, setError] = React.useState<FieldError>(undefined); + const setState = React.useCallback((newValue: A, newError: FieldError) => { const localError = validate(newValue, checker) || newError; setValue(newValue); setError(localError); @@ -133,9 +133,9 @@ export function useValid<A>( ): FieldState<A> { const [value, setValue] = state; const [local, setLocal] = React.useState(value); - const [error, setError] = React.useState<Error>(undefined); + const [error, setError] = React.useState<FieldError>(undefined); const update = React.useCallback( - (newValue: A, newError: Error) => { + (newValue: A, newError: FieldError) => { setLocal(newValue); setError(newError); if (!newError) setValue(newValue); @@ -162,7 +162,7 @@ export function useDefined<A>( ): FieldState<A | undefined> { const [value, error, setState] = state; const update = React.useCallback( - (newValue: A | undefined, newError: Error) => { + (newValue: A | undefined, newError: FieldError) => { if (newValue !== undefined) { setState(newValue, newError); } @@ -182,7 +182,7 @@ export function useRequired<A>( const [value, error, setState] = state; const cache = React.useRef(value); const update = React.useCallback( - (newValue: A | undefined, newError: Error) => { + (newValue: A | undefined, newError: FieldError) => { if (newValue === undefined) { setState(cache.current, onError || 'Required field'); } else { @@ -202,7 +202,7 @@ export function useChecker<A>( checker?: Checker<A>, ): FieldState<A> { const [value, error, setState] = state; - const update = React.useCallback((newValue: A, newError: Error) => { + const update = React.useCallback((newValue: A, newError: FieldError) => { const localError = validate(newValue, checker) || newError; setState(newValue, localError); }, [checker, setState]); @@ -235,11 +235,11 @@ export function useFilter<A, B>( const [value, error, setState] = state; const [localValue, setLocalValue] = React.useState(defaultValue); - const [localError, setLocalError] = React.useState<Error>(undefined); + const [localError, setLocalError] = React.useState<FieldError>(undefined); const [dangling, setDangling] = React.useState(false); const update = React.useCallback( - (newValue: B, newError: Error) => { + (newValue: B, newError: FieldError) => { try { const outValue = output(newValue); setLocalValue(newValue); @@ -250,7 +250,7 @@ export function useFilter<A, B>( } } catch (err) { setLocalValue(newValue); - setLocalError(newError || err.toString() || 'Invalid value'); + setLocalError(newError || err ? '' + err : 'Invalid value'); setDangling(true); } }, [output, setState, setLocalValue, setLocalError], @@ -262,7 +262,7 @@ export function useFilter<A, B>( try { return [input(value), error, update]; } catch (err) { - return [localValue, err.toString() || 'Invalid input', update]; + return [localValue, err ? '' + err : 'Invalid input', update]; } } @@ -284,12 +284,12 @@ export function useLatency<A>( const update = React.useMemo(() => { if (period > 0) { const propagate = debounce( - (lateValue: A, lateError: Error) => { + (lateValue: A, lateError: FieldError) => { setState(lateValue, lateError); setDangling(false); }, period, ); - return (newValue: A, newError: Error) => { + return (newValue: A, newError: FieldError) => { setLocalValue(newValue); setLocalError(newError); setDangling(true); @@ -315,7 +315,7 @@ export function useProperty<A, K extends keyof A>( checker?: Checker<A[K]>, ): FieldState<A[K]> { const [value, error, setState] = state; - const update = React.useCallback((newProp: A[K], newError: Error) => { + const update = React.useCallback((newProp: A[K], newError: FieldError) => { const newValue = { ...value, [property]: newProp }; const objError = isObjectError(error) ? error : {}; const propError = validate(newProp, checker) || newError; @@ -334,7 +334,7 @@ export function useIndex<A>( checker?: Checker<A>, ): FieldState<A> { const [array, error, setState] = state; - const update = React.useCallback((newValue: A, newError: Error) => { + const update = React.useCallback((newValue: A, newError: FieldError) => { const newArray = array.slice(); newArray[index] = newValue; const localError = isArrayError(error) ? error.slice() : []; @@ -436,7 +436,7 @@ export interface WarningProps { /** Short warning message (displayed on hover). */ warning?: string; /** Error details (if a string is provided, in tooltip). */ - error?: Error; + error?: FieldError; /** Label offset. */ offset?: number; } @@ -494,7 +494,7 @@ export interface SectionProps extends FilterProps, Children { /** Warning Error (when unfolded). */ warning?: string; /** Associated Error. */ - error?: Error; + error?: FieldError; /** Fold/Unfold settings. */ settings?: string; /** Fold/Unfold state (defaults to false). */ @@ -550,7 +550,7 @@ export interface GenericFieldProps extends FilterProps, Children { /** Warning message (in case of error). */ onError?: string; /** Error (if any). */ - error?: Error; + error?: FieldError; } let FIELDID = 0; @@ -626,7 +626,7 @@ export interface FieldProps<A> extends FilterProps { } type InputEvent = { target: { value: string } }; -type InputState = [string, Error, (evt: InputEvent) => void]; +type InputState = [string, FieldError, (evt: InputEvent) => void]; function useChangeEvent(setState: Callback<string>) { return React.useCallback( @@ -635,17 +635,6 @@ function useChangeEvent(setState: Callback<string>) { ); } -function useTextInputField( - props: TextFieldProps, - defaultLatency: number, -): InputState { - const checked = useChecker(props.state, props.checker); - const period = props.latency ?? defaultLatency; - const [value, error, setState] = useLatency(checked, period); - const onChange = useChangeEvent(setState); - return [value || '', error, onChange]; -} - /* --------------------------------------------------------------------------*/ /* --- Text Fields ---*/ /* --------------------------------------------------------------------------*/ @@ -658,6 +647,17 @@ export interface TextFieldProps extends FieldProps<string | undefined> { latency?: number; } +function useTextInputField( + props: TextFieldProps, + defaultLatency: number, +): InputState { + const checked = useChecker(props.state, props.checker); + const period = props.latency ?? defaultLatency; + const [value, error, setState] = useLatency(checked, period); + const onChange = useChangeEvent(setState); + return [value || '', error, onChange]; +} + /** Text Field. @category Text Fields diff --git a/ivette/src/dome/renderer/table/views.tsx b/ivette/src/dome/renderer/table/views.tsx index 2c63f9793913c048788823025728365a01006a13..55fddd2c7461206b42cea9c972f658e6e6fa487b 100644 --- a/ivette/src/dome/renderer/table/views.tsx +++ b/ivette/src/dome/renderer/table/views.tsx @@ -206,8 +206,8 @@ interface PopupItem { type PopupMenu = ('separator' | PopupItem)[]; type Cmap<A> = Map<string, A>; -type Cprops = ColProps<any>; type ColProps<R> = ColumnProps<R, any>; +type Cprops = ColProps<any>; // -------------------------------------------------------------------------- // --- Column Utilities @@ -239,8 +239,8 @@ function makeRowGetter<Key, Row>(model?: Model<Key, Row>) { } function makeDataGetter( - getter: ((row: any, dataKey: string) => any) = defaultGetter, dataKey: string, + getter: ((row: any, dataKey: string) => any) = defaultGetter, ): TableCellDataGetter { return (({ rowData }) => { try { @@ -644,7 +644,7 @@ class TableState<Key, Row> { computeGetter(id: string, dataKey: string, props: Cprops) { const current = this.getter.get(id); if (current) return current; - const dataGetter = makeDataGetter(props.getter, dataKey); + const dataGetter = makeDataGetter(dataKey, props.getter); this.getter.set(id, dataGetter); return dataGetter; } diff --git a/ivette/src/dome/renderer/text/buffers.ts b/ivette/src/dome/renderer/text/buffers.ts index 712089fa924a872903f7903c867cd41b43dfdecd..5fe857bd0dd994b3c459503ff52c3b7a79745cd9 100644 --- a/ivette/src/dome/renderer/text/buffers.ts +++ b/ivette/src/dome/renderer/text/buffers.ts @@ -113,6 +113,8 @@ function byVisibleTag(lmin: number, lmax: number) { // --- Buffer // -------------------------------------------------------------------------- +type TextMarker = CodeMirror.TextMarker<CodeMirror.MarkerRange>; + export interface RichTextBufferProps { /** @@ -184,7 +186,7 @@ export class RichTextBuffer extends Emitter { private cssmarkers = new Map<string, CSSMarker>(); // Indexed by marker user identifier - private textmarkers = new Map<string, CodeMirror.TextMarker[]>(); + private textmarkers = new Map<string, TextMarker[]>(); private decorator?: Decorator; private edited = false; @@ -327,7 +329,7 @@ export class RichTextBuffer extends Emitter { /** Lookup for the text markers associated with a marker identifier. Remove the marked tags from the buffered tag array. */ - findTextMarker(id: string): CodeMirror.TextMarker[] { + findTextMarker(id: string): TextMarker[] { this.doFlushText(); this.bufferedTags.forEach((tg, idx) => { if (tg?.id === id) { @@ -470,8 +472,10 @@ export class RichTextBuffer extends Emitter { let line = Infinity; this.findTextMarker(position).forEach((tm) => { const rg = tm.find(); - const ln = rg.from.line; - if (ln < line) line = ln; + if (rg && rg.from) { + const ln = rg.from.line; + if (ln < line) line = ln; + } }); if (line !== Infinity) this.emit('scroll', line); @@ -497,7 +501,7 @@ export class RichTextBuffer extends Emitter { private onChange( _editor: CodeMirror.Editor, - change: CodeMirror.EditorChangeLinkedList, + change: CodeMirror.EditorChange, ) { if (change.origin !== 'buffer') { this.setEdited(true); diff --git a/ivette/src/dome/renderer/text/editors.tsx b/ivette/src/dome/renderer/text/editors.tsx index ffa7c9193c00ea62ad7f9aebe63099b80cfc1a5e..cb9036dc52f980dcf660d3c4ee9db864a12b0722 100644 --- a/ivette/src/dome/renderer/text/editors.tsx +++ b/ivette/src/dome/renderer/text/editors.tsx @@ -386,7 +386,7 @@ class CodeMirrorWrapper extends React.Component<TextProps> { // --- Focus // -------------------------------------------------------------------------- - handleKey(_cm: CodeMirror.Editor, key: string, _evt: KeyboardEvent) { + handleKey(_cm: CodeMirror.Editor, key: string, _evt: Event) { switch (key) { case 'Esc': this.props.buffer?.setFocused(false); diff --git a/ivette/src/frama-c/client_socket.ts b/ivette/src/frama-c/client_socket.ts index 23777c7a5f2f4c742c095202d4662c2c24648ee1..55be45c292f1bae5df6f3d9cb8eb762c812d31dd 100644 --- a/ivette/src/frama-c/client_socket.ts +++ b/ivette/src/frama-c/client_socket.ts @@ -21,18 +21,19 @@ /* ************************************************************************ */ import Net from 'net'; +import { Debug } from 'dome'; import Emitter from 'events'; import { json } from 'dome/data/json'; import { Client } from './client'; +const D = new Debug('SocketServer'); + // -------------------------------------------------------------------------- // --- Frama-C Server API // -------------------------------------------------------------------------- class SocketClient implements Client { - constructor() { } - events = new Emitter(); running = false; socket: Net.Socket | undefined; @@ -45,7 +46,7 @@ class SocketClient implements Client { this.socket.destroy(); } this.socket = Net.createConnection(sockaddr, () => { - console.log('Socket server connected'); + D.log('Client connected'); this.running = true; this._flush(); }); @@ -53,14 +54,14 @@ class SocketClient implements Client { this.socket.on('end', () => this.disconnect()); this.socket.on('data', (data: Buffer) => this._receive(data)); this.socket.on('error', (err: Error) => { - console.warn('Socket error', err); + D.warn('Socket error', err); }); } disconnect(): void { this.queue = []; if (this.socket) { - console.log('Socket disconnected'); + D.log('Client disconnected'); this.socket.destroy(); this.socket = undefined; } @@ -73,19 +74,34 @@ class SocketClient implements Client { } /** Signal ON */ - sigOn(id: string): void { this.queue.push({ cmd: 'SIGON', id }); this._flush(); } + sigOn(id: string): void { + this.queue.push({ cmd: 'SIGON', id }); + this._flush(); + } /** Signal ON */ - sigOff(id: string): void { this.queue.push({ cmd: 'SIGOFF', id }); this._flush(); } + sigOff(id: string): void { + this.queue.push({ cmd: 'SIGOFF', id }); + this._flush(); + } /** Kill Request */ - kill(id: string): void { this.queue.push({ cmd: 'KILL', id }); this._flush(); } + kill(id: string): void { + this.queue.push({ cmd: 'KILL', id }); + this._flush(); + } /** Polling */ - poll(): void { this.queue.push('POLL'); this._flush(); } + poll(): void { + this.queue.push('POLL'); + this._flush(); + } /** Shutdown the server */ - shutdown(): void { this.queue.push('SHUTDOWN'); this._flush(); } + shutdown(): void { + this.queue.push('SHUTDOWN'); + this._flush(); + } /** Request data callback */ onData(callback: (id: string, data: json) => void): void { @@ -163,7 +179,7 @@ class SocketClient implements Client { _receive(chunk: Buffer) { this.buffer = Buffer.concat([this.buffer, chunk]); - while (1) { + while (true) { const data = this._fetch(); if (data === undefined) break; try { @@ -176,12 +192,12 @@ class SocketClient implements Client { case 'REJECTED': this.events.emit('REJECT', cmd.id); break; case 'SIGNAL': this.events.emit('SIGNAL', cmd.id); break; default: - console.log('Unknown command', cmd); + D.warn('Unknown command', cmd); } } else - console.log('Misformed data', data); + D.warn('Misformed data', data); } catch (err) { - console.log('Misformed JSON', data, err); + D.warn('Misformed JSON', data, err); } } } diff --git a/ivette/src/frama-c/kernel/ASTview.tsx b/ivette/src/frama-c/kernel/ASTview.tsx index 76879dcfca9675d4aa1aeffa73711ae6ac734aac..a925d593f89e13bb061e2fa50be05b93d9c5f879 100644 --- a/ivette/src/frama-c/kernel/ASTview.tsx +++ b/ivette/src/frama-c/kernel/ASTview.tsx @@ -186,9 +186,11 @@ export default function ASTview() { const markers = buffer.findTextMarker(prop.key); markers.forEach((marker) => { const pos = marker.find(); - buffer.forEach((cm) => { - cm.setGutterMarker(pos.from.line, 'bullet', bullet); - }); + if (pos) { + buffer.forEach((cm) => { + cm.setGutterMarker(pos.from.line, 'bullet', bullet); + }); + } }); } } diff --git a/ivette/src/frama-c/kernel/Globals.tsx b/ivette/src/frama-c/kernel/Globals.tsx index 6c94aedd8590d7a3f1b80d699a6f1fadaa192c48..49799d3ef0f8e4f249f6e7c6a4f4b3bf85909a4a 100644 --- a/ivette/src/frama-c/kernel/Globals.tsx +++ b/ivette/src/frama-c/kernel/Globals.tsx @@ -119,7 +119,7 @@ export default () => { function isSelected(fct: functionsData) { return multipleSelection?.allSelections.some( - (l) => fct.name === l?.fct + (l) => fct.name === l?.fct, ); } diff --git a/ivette/src/frama-c/kernel/Messages.tsx b/ivette/src/frama-c/kernel/Messages.tsx index c215c943ff3f962b846f3ef0854654311c94df12..68b1a885b96aefea21daaf656c494b75acf60b15 100644 --- a/ivette/src/frama-c/kernel/Messages.tsx +++ b/ivette/src/frama-c/kernel/Messages.tsx @@ -53,7 +53,7 @@ interface Search { } type KindFilter = Record<logkind, boolean>; -type PluginFilter = {[key: string]: boolean}; +type PluginFilter = { [key: string]: boolean }; type EmitterFilter = { kernel: boolean; plugins: PluginFilter; @@ -80,23 +80,23 @@ const kindFilter: KindFilter = { /* The fields must be exactly the short names of Frama-C plugins used in messages. They are all shown by default. */ const pluginFilter: PluginFilter = { - aorai: true, - dive: true, + 'aorai': true, + 'dive': true, 'e-acsl': true, - eva: true, - from: true, - impact: true, - inout: true, - metrics: true, - nonterm: true, - pdg: true, - report: true, - rte: true, - scope: true, - server: true, - slicing: true, - variadic: true, - wp: true, + 'eva': true, + 'from': true, + 'impact': true, + 'inout': true, + 'metrics': true, + 'nonterm': true, + 'pdg': true, + 'report': true, + 'rte': true, + 'scope': true, + 'server': true, + 'slicing': true, + 'variadic': true, + 'wp': true, }; const emitterFilter = { @@ -168,7 +168,7 @@ function searchString(search: string | undefined, msg: string) { function filterSearched(search: Search, msg: Message) { return (searchString(search.message, msg.message) && - searchCategory(search.category, msg.category)); + searchCategory(search.category, msg.category)); } function filterFunction(filter: Filter, kf: string | undefined, msg: Message) { @@ -179,20 +179,15 @@ function filterFunction(filter: Filter, kf: string | undefined, msg: Message) { function filterMessage(filter: Filter, kf: string | undefined, msg: Message) { return (filterFunction(filter, kf, msg) && - filterSearched(filter.search, msg) && - filterKind(filter.kind, msg) && - filterEmitter(filter.emitter, msg)); + filterSearched(filter.search, msg) && + filterKind(filter.kind, msg) && + filterEmitter(filter.emitter, msg)); } // -------------------------------------------------------------------------- // --- Filters panel and ratio // -------------------------------------------------------------------------- -function Checkbox(p: Forms.CheckboxFieldProps) { - const lbl = p.label.charAt(0).toUpperCase() + p.label.slice(1).toLowerCase(); - return <Forms.CheckboxField label={lbl} state={p.state} />; -} - function Section(p: Forms.SectionProps) { const settings = `ivette.messages.filter.${p.label}`; return ( @@ -202,28 +197,42 @@ function Section(p: Forms.SectionProps) { ); } -function MessageFilter(props: {filter: State<Filter>}) { +function MessageKindCheckbox(props: { + kind: logkind, + kindState: Forms.FieldState<KindFilter>, +}) { + const { kind, kindState } = props; + const label = kind.charAt(0).toUpperCase + kind.slice(1).toLowerCase(); + const state = Forms.useProperty(kindState, kind); + return <Forms.CheckboxField label={label} state={state} />; +} + +function PluginCheckbox(props: { + plugin: string, + pluginState: Forms.FieldState<PluginFilter>, +}) { + const label = props.plugin.toUpperCase(); + const state = Forms.useProperty(props.pluginState, props.plugin); + return <Forms.CheckboxField label={label} state={state} />; +} + +function MessageFilter(props: { filter: State<Filter> }) { const state = Forms.useValid(props.filter); const search = Forms.useProperty(state, 'search'); const categoryState = Forms.useProperty(search, 'category'); const messageState = Forms.useProperty(search, 'message'); - - const kind = Forms.useProperty(state, 'kind'); - const kindState = (key: logkind) => Forms.useProperty(kind, key); + const kindState = Forms.useProperty(state, 'kind'); const kindCheckboxes = - Object.keys(kindFilter).map((key) => ( - <Checkbox key={key} label={key} state={kindState(key as logkind)} /> + Object.keys(kindFilter).map((k) => ( + <MessageKindCheckbox key={k} kind={k as logkind} kindState={kindState} /> )); - - const emitter = Forms.useProperty(state, 'emitter'); - function EmitterCheckbox(p: {key: 'kernel' | 'others'}) { - return <Checkbox label={p.key} state={Forms.useProperty(emitter, p.key)} />; - } - const plugin = Forms.useProperty(emitter, 'plugins'); - const pluginState = (key: string) => Forms.useProperty(plugin, key); + const emitterState = Forms.useProperty(state, 'emitter'); + const kernelState = Forms.useProperty(emitterState, 'kernel'); + const othersState = Forms.useProperty(emitterState, 'others'); + const pluginState = Forms.useProperty(emitterState, 'plugins'); const pluginCheckboxes = - Object.keys(pluginFilter).map((key) => ( - <Checkbox key={key} label={key} state={pluginState(key)} /> + Object.keys(pluginFilter).map((p) => ( + <PluginCheckbox key={p} plugin={p} pluginState={pluginState} /> )); return ( @@ -239,29 +248,29 @@ function MessageFilter(props: {filter: State<Filter>}) { state={categoryState} placeholder="Category" title={'Search in message category.\n' - + 'Use -<name> to hide some categories.'} + + 'Use -<name> to hide some categories.'} /> <Forms.TextField label="Message" state={messageState} placeholder="Message" title={'Search in message text.\n' - + 'Case-insensitive by default.\n' - + 'Use "text" for an exact case-sensitive search.'} + + 'Case-insensitive by default.\n' + + 'Use "text" for an exact case-sensitive search.'} /> </Section> <Section label="Kind"> - { kindCheckboxes } + {kindCheckboxes} </Section> <Section label="Emitter"> <div className="message-emitter-category"> - { EmitterCheckbox({ key: 'kernel' }) } + <Forms.CheckboxField label='Kernel' state={kernelState} /> </div> <div className="message-emitter-category"> - { pluginCheckboxes } + {pluginCheckboxes} </div> <div className="message-emitter-category"> - { EmitterCheckbox({ key: 'others' }) } + <Forms.CheckboxField label='Others' state={othersState} /> </div> </Section> </Forms.Page> diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx index 4a55284e9eb085403daead8427f2142eab05ce87..a6100ca4aa4daf64774c17888f7e710fd9ddf319 100644 --- a/ivette/src/frama-c/kernel/SourceCode.tsx +++ b/ivette/src/frama-c/kernel/SourceCode.tsx @@ -90,10 +90,14 @@ 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 = () => System.readFile(file).catch(onError); - const text = React.useMemo(read, [file, onError]); + const text = React.useMemo(async () => { + const onError = () => { + if (file) + D.error(`Fail to load source code file ${file}`); + return ''; + }; + return System.readFile(file).catch(onError); + }, [file]); const { result } = Dome.usePromise(text); React.useEffect(() => buffer.setValue(result), [buffer, result]); @@ -115,29 +119,31 @@ export default function SourceCode() { type position = CodeMirror.Position; type editor = CodeMirror.Editor; - async function select(editor: editor, event: MouseEvent) { - const pos = editor.coordsChar({ left: event.x, top: event.y }); - if (file === '' || !pos) return; - const arg = [file, pos.line + 1, pos.ch + 1]; - Server - .send(getMarkerAt, arg) - .then(([fct, marker]) => { - if (fct || marker) { - const location = { fct, marker } as States.Location; - selected.current = location; - updateSelection({ location }); - } - }) - .catch((err) => { - D.error(`Failed to get marker from source file position: ${err}`); - Status.setMessage({ - text: 'Failed request to Frama-C server', - kind: 'error', + const selectCallback = React.useCallback( + async function select(editor: editor, event: MouseEvent) { + const pos = editor.coordsChar({ left: event.x, top: event.y }); + if (file === '' || !pos) return; + const arg = [file, pos.line + 1, pos.ch + 1]; + Server + .send(getMarkerAt, arg) + .then(([fct, marker]) => { + if (fct || marker) { + const location = { fct, marker } as States.Location; + selected.current = location; + updateSelection({ location }); + } + }) + .catch((err) => { + D.error(`Failed to get marker from source file position: ${err}`); + Status.setMessage({ + text: 'Failed request to Frama-C server', + kind: 'error', + }); }); - }); - } + }, + [file, updateSelection], + ); - const selectCallback = React.useCallback(select, [file]); React.useEffect(() => { buffer.forEach((cm) => cm.on('mousedown', selectCallback)); return () => buffer.forEach((cm) => cm.off('mousedown', selectCallback)); diff --git a/ivette/src/frama-c/kernel/Status.tsx b/ivette/src/frama-c/kernel/Status.tsx index 14c31a6fd0e528abd4224c1a393865b1bf66f9ea..7c0f6ecec41e68267d03bb32e4f3ac580e1afc85 100644 --- a/ivette/src/frama-c/kernel/Status.tsx +++ b/ivette/src/frama-c/kernel/Status.tsx @@ -34,17 +34,17 @@ import { GlobalState, useGlobalState } from 'dome/data/states'; export type kind = 'none' | 'info' | 'warning' | 'error' | 'success' | 'progress'; -export interface Message { +export interface MessageProps { kind: kind; text: string; title?: string; } -const emptyMessage: Message = { text: '', kind: 'none' }; +const emptyMessage: MessageProps = { text: '', kind: 'none' }; const GlobalMessage = new GlobalState(emptyMessage); -export function setMessage(message: Message) { +export function setMessage(message: MessageProps) { GlobalMessage.setValue(message); } @@ -54,11 +54,11 @@ export default function Message() { return ( <> <Toolbars.Space /> - { message.kind === 'progress' && <LED status="active" blink /> } - { message.kind === 'success' && <Icon id="CHECK" fill="green" /> } - { message.kind === 'warning' && <Icon id="ATTENTION" /> } - { message.kind === 'error' && <Icon id="CROSS" fill="red" /> } - { message.kind === 'info' && <Icon id="CIRC.INFO" /> } + {message.kind === 'progress' && <LED status="active" blink />} + {message.kind === 'success' && <Icon id="CHECK" fill="green" />} + {message.kind === 'warning' && <Icon id="ATTENTION" />} + {message.kind === 'error' && <Icon id="CROSS" fill="red" />} + {message.kind === 'info' && <Icon id="CIRC.INFO" />} <Code label={message.text} title={message.title} /> <Toolbars.Space /> <IconButton diff --git a/ivette/src/frama-c/plugins/dive/index.tsx b/ivette/src/frama-c/plugins/dive/index.tsx index e121ae5a9b4a92bada905cc84a234f420a3f70e1..a1e890826be099025e3486dc2bcbd32624bb5ae2 100644 --- a/ivette/src/frama-c/plugins/dive/index.tsx +++ b/ivette/src/frama-c/plugins/dive/index.tsx @@ -217,10 +217,11 @@ class Dive { trigger: 'manual', appendTo: document.body, lazy: false, - onCreate: (instance: Tippy.Instance) => { + onCreate: (instance: any) => { const { popperInstance } = instance; - if (popperInstance) + if (popperInstance) { popperInstance.reference = (node as any).popperRef(); + } }, }; @@ -231,7 +232,8 @@ class Dive { ...common, content: node.data().values, placement: 'top', - distance: 10, + //distance: 10, + offset: [10, 10], arrow: true, })); } @@ -241,7 +243,8 @@ class Dive { ...common, content: node.data().type, placement: 'bottom', - distance: 20, + //distance: 20, + offset: [20, 20], theme: 'light-border', arrow: false, })); diff --git a/ivette/src/frama-c/plugins/eva/CoverageMeter.tsx b/ivette/src/frama-c/plugins/eva/CoverageMeter.tsx index 39537a19e5caa644252dad7992792ca5405b203c..a6468e552e62c2c1d11c263787bf3a4818ca502e 100644 --- a/ivette/src/frama-c/plugins/eva/CoverageMeter.tsx +++ b/ivette/src/frama-c/plugins/eva/CoverageMeter.tsx @@ -22,17 +22,17 @@ import React from 'react'; -export interface Coverage { +export interface CoverageProps { reachable: number; dead: number; } -export function percent(coverage: Coverage): string { +export function percent(coverage: CoverageProps): string { const q = coverage.reachable / (coverage.reachable + coverage.dead); return `${(q * 100).toFixed(1)}%`; } -export default function (props: {coverage: Coverage}) { +export default function(props: { coverage: CoverageProps }) { const { reachable, dead } = props.coverage; const total = reachable + dead; diff --git a/ivette/src/frama-c/plugins/eva/valueinfos.tsx b/ivette/src/frama-c/plugins/eva/valueinfos.tsx index cd4d920d03361186691bafde4361d4ec3244eb0f..817e64e5f7560d9707fdf4096a5c6b33dfa8a167 100644 --- a/ivette/src/frama-c/plugins/eva/valueinfos.tsx +++ b/ivette/src/frama-c/plugins/eva/valueinfos.tsx @@ -48,9 +48,10 @@ interface StmtProps { } export function Stmt(props: StmtProps) { + const markersInfo = States.useSyncArray(Ast.markerInfo); + const { stmt, marker, short } = props; if (!stmt) return null; - const markersInfo = States.useSyncArray(Ast.markerInfo); const line = markersInfo.getData(marker)?.sloc?.line; const filename = markersInfo.getData(marker)?.sloc?.base; const title = markersInfo.getData(stmt)?.descr; diff --git a/ivette/src/frama-c/server.ts b/ivette/src/frama-c/server.ts index feebcd4a9d9018864c913dd5dd08e4866463a154..5c2ba0038e98cfab8a8e3f9299915002240374ed 100644 --- a/ivette/src/frama-c/server.ts +++ b/ivette/src/frama-c/server.ts @@ -37,7 +37,7 @@ import * as System from 'dome/system'; import * as Json from 'dome/data/json'; import { RichTextBuffer } from 'dome/text/buffers'; import { ChildProcess } from 'child_process'; -import { client } from './client_zmq'; +import { client } from './client_socket'; // -------------------------------------------------------------------------- // --- Pretty Printing (Browser Console) @@ -100,7 +100,7 @@ export enum Stage { /** Server is restarting. */ RESTARTING = 'RESTARTING', /** Server is off upon failure. */ - FAILURE = 'FAILURE' + FAILURE = 'FAILURE', } export interface OkStatus { @@ -248,9 +248,10 @@ export async function start() { await _launch(); _status(okStatus(Stage.ON)); } catch (error) { - D.error(error.toString()); - buffer.append(error.toString(), '\n'); - _exit(error); + const msg = '' + error; + D.error(msg); + buffer.append(msg, '\n'); + _exit(msg); } return; case Stage.HALTING: @@ -480,14 +481,14 @@ async function _launch() { if (signal) { // [signal] is non-null. buffer.log('Signal:', signal); - const error = new Error(`Process terminated by the signal ${signal}`); + const error = `Process terminated by the signal ${signal}`; _exit(error); return; } // [signal] is null, hence [code] is non-null (cf. NodeJS doc). if (code) { buffer.log('Exit:', code); - const error = new Error(`Process exited with code ${code}`); + const error = `Process exited with code ${code}`; _exit(error); } else { // [code] is zero: normal exit w/o error. @@ -543,14 +544,14 @@ async function _shutdown() { await killingPromise; } -function _exit(error?: Error) { +function _exit(error?: string) { _reset(); client.disconnect(); process = undefined; if (status.stage === Stage.RESTARTING) { setImmediate(start); } else if (error) { - _status(errorStatus(error.toString())); + _status(errorStatus(error)); } else { _status(okStatus(Stage.OFF)); } @@ -565,6 +566,7 @@ class SignalHandler { event: Dome.Event; active: boolean; listen: boolean; + handler: _.DebouncedFunc<() => void>; constructor(id: string) { this.id = id; @@ -572,7 +574,7 @@ class SignalHandler { this.active = false; this.listen = false; this.sigon = this.sigon.bind(this); - this.sigoff = _.debounce(this.sigoff.bind(this), 1000); + this.sigoff = this.handler = _.debounce(this.sigoff.bind(this), 1000); this.unplug = this.unplug.bind(this); } @@ -617,6 +619,7 @@ class SignalHandler { unplug() { this.listen = false; + this.handler.cancel(); } } @@ -633,9 +636,7 @@ function _signal(id: string): SignalHandler { return s; } -client.onSignal((id: string) => { - _signal(id).event.emit(); -}) +client.onSignal((id: string) => { _signal(id).event.emit(); }); // --- External API @@ -686,7 +687,6 @@ READY.on(() => { SHUTDOWN.on(() => { signals.forEach((h: SignalHandler) => { h.unplug(); - (h.sigoff as unknown as _.Cancelable).cancel(); }); }); @@ -701,7 +701,7 @@ export enum RqKind { /** Used to write data into the Frama-C server. */ SET = 'SET', /** Used to make the Frama-C server execute a task. */ - EXEC = 'EXEC' + EXEC = 'EXEC', } /** Server signal. */ @@ -726,7 +726,7 @@ export type GetRequest<In, Out> = Request<RqKind.GET, In, Out>; export type SetRequest<In, Out> = Request<RqKind.SET, In, Out>; export type ExecRequest<In, Out> = Request<RqKind.EXEC, In, Out>; -export interface Killable<Data> extends Promise<Data> { +export interface Response<Data> extends Promise<Data> { kill?: () => void; } @@ -739,23 +739,30 @@ export interface Killable<Data> extends Promise<Data> { export function send<In, Out>( request: Request<RqKind, In, Out>, param: In, -): Killable<Out> { +): Response<Out> { if (!isRunning()) return Promise.reject(new Error('Server not running')); if (!request.name) return Promise.reject(new Error('Undefined request')); const rid = `RQ.${rqCount}`; rqCount += 1; - const promise: Killable<Out> = new Promise<Out>((resolve, reject) => { + const response: Response<Out> = new Promise<Out>((resolve, reject) => { const decodedResolve = (js: Json.json) => { - const result = Json.jTry(request.output)(js); - resolve(result); + try { + const result = request.output(js); + if (result !== undefined) + resolve(result); + else + reject(); + } catch (err) { + reject(err); + } }; pending[rid] = [decodedResolve, reject]; }); - promise.kill = () => { + response.kill = () => { if (pending[rid]) client.kill(rid); }; client.send(request.kind, rid, request.name, param); - return promise; + return response; } client.onData((id: string, data: Json.json) => { diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index e5d85c61c9a872f66c4e094b36df3e3b119dc4e3..a3c5081f6481e0e0f8fe49c82e4efe5907d7009e 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -72,7 +72,7 @@ Server.onReady(async () => { currentProject = current.id; PROJECT.emit(); } catch (error) { - D.error(`Fail to retrieve the current project. ${error.toString()}`); + D.error(`Fail to retrieve the current project. ${error}`); } }); @@ -116,7 +116,7 @@ export async function setProject(project: string) { currentProject = project; PROJECT.emit(); } catch (error) { - D.error(`Fail to set the current project. ${error.toString()}`); + D.error(`Fail to set the current project. ${error}`); } } } @@ -171,7 +171,7 @@ export function useRequest<In, Out>( const r = await Server.send(rq, params); update(r); } catch (error) { - D.error(`Fail in useRequest '${rq.name}'. ${error.toString()}`); + D.error(`Fail in useRequest '${rq.name}'. ${error}`); update(options.onError); } } else { @@ -300,7 +300,7 @@ class SyncState<A> { } catch (error) { D.error( `Fail to set value of SyncState '${this.handler.name}'.`, - `${error.toString()}`, + `${error}`, ); } } @@ -314,7 +314,7 @@ class SyncState<A> { } catch (error) { D.error( `Fail to update SyncState '${this.handler.name}'.`, - `${error.toString()}`, + `${error}`, ); } } @@ -407,7 +407,7 @@ class SyncArray<K, A> { } catch (error) { D.error( `Fail to retrieve the value of syncArray '${this.handler.name}.`, - `${error.toString()}`, + `${error}`, ); } finally { this.fetching = false; @@ -426,7 +426,7 @@ class SyncArray<K, A> { } catch (error) { D.error( `Fail to set reload of syncArray '${this.handler.name}'.`, - `${error.toString()}`, + `${error}`, ); } } @@ -477,7 +477,7 @@ export function useSyncArray<K, A>( ): CompactModel<K, A> { Dome.useUpdate(PROJECT); const st = lookupSyncArray(arr); - React.useEffect(st.update); + React.useEffect(() => st.update(), [st]); Server.useSignal(arr.signal, st.fetch); useModel(st.model, sync); return st.model; @@ -778,7 +778,7 @@ export function useSelection(): [Selection, (a: SelectionActions) => void] { /** Resets the selected locations. */ export async function resetSelection() { GlobalSelection.setValue(emptySelection); - const main = await Server.send(Ast.getMainFunction, { }); + const main = await Server.send(Ast.getMainFunction, {}); // If the selection has already been modified, do not change it. if (main && GlobalSelection.getValue() === emptySelection) { GlobalSelection.setValue({ ...emptySelection, current: { fct: main } }); diff --git a/ivette/src/renderer/Laboratory.tsx b/ivette/src/renderer/Laboratory.tsx index 3a41a613e3f2c0a661fcb76ce68272f63e3c1edb..cf3ce8cd5cec0db6eaa8f41ec5351746a210673c 100644 --- a/ivette/src/renderer/Laboratory.tsx +++ b/ivette/src/renderer/Laboratory.tsx @@ -51,8 +51,8 @@ const UPDATE = new Dome.Event('labview.library'); class Library { modified: boolean; - virtual: {}; - collection: {}; + virtual: any; + collection: any; items: any[]; constructor() {