diff --git a/src/alerts.ts b/src/alerts.ts index 807230532..318fd175b 100644 --- a/src/alerts.ts +++ b/src/alerts.ts @@ -6,6 +6,7 @@ import alerts from './alerts/alerts'; import ble from './ble/alerts'; import explorer from './explorer/alerts'; import firmware from './firmware/alerts'; +import mpy from './mpy/alerts'; import type { CreateToast } from './toasterTypes'; /** This collects alerts from all of the subsystems of the app */ @@ -14,6 +15,7 @@ const alertDomains = { ble, explorer, firmware, + mpy, }; /** Gets the type of available alert domains. */ diff --git a/src/editor/actions.ts b/src/editor/actions.ts index b770c970f..1db7854e1 100644 --- a/src/editor/actions.ts +++ b/src/editor/actions.ts @@ -112,3 +112,12 @@ export const editorDidFailToActivateFile = createAction((uuid: UUID, error: Erro uuid, error, })); + +/** + * Requests to activate a file and show line. + */ +export const editorGoto = createAction((uuid: UUID, line: number) => ({ + type: 'editor.action.goto', + uuid, + line, +})); diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts index ef72fdcb4..6bab2f48c 100644 --- a/src/editor/sagas.ts +++ b/src/editor/sagas.ts @@ -60,6 +60,7 @@ import { editorDidOpenFile, editorGetValueRequest, editorGetValueResponse, + editorGoto, editorOpenFile, } from './actions'; import { EditorError } from './error'; @@ -265,6 +266,34 @@ function* handleEditorActivateFile( } } +function* handleEditorGoto( + editor: monaco.editor.ICodeEditor, + action: ReturnType, +): Generator { + yield* put(editorActivateFile(action.uuid)); + + const { didActivate, didFailToActivate } = yield* race({ + didActivate: take(editorDidActivateFile.when((a) => a.uuid === action.uuid)), + didFailToActivate: take( + editorDidFailToActivateFile.when((a) => a.uuid === action.uuid), + ), + }); + + if (didFailToActivate) { + return; + } + + defined(didActivate); + + editor.revealLineInCenterIfOutsideViewport(action.line); + editor.setSelection({ + startColumn: 1, + startLineNumber: action.line, + endColumn: Infinity, + endLineNumber: action.line, + }); +} + function* handleEditorDidCloseFile( activeFileHistory: ActiveFileHistoryManager, action: ReturnType, @@ -356,6 +385,7 @@ function* handleDidCreateEditor(editor: monaco.editor.ICodeEditor): Generator { openFiles, activeFileHistory, ); + yield* takeEvery(editorGoto, handleEditorGoto, editor); yield* takeEvery(editorDidCloseFile, handleEditorDidCloseFile, activeFileHistory); yield* fork(monitorViewState, editor); diff --git a/src/fileStorage/hooks.ts b/src/fileStorage/hooks.ts index 1a9c28a85..40cb36ac2 100644 --- a/src/fileStorage/hooks.ts +++ b/src/fileStorage/hooks.ts @@ -25,3 +25,18 @@ export function useFileStoragePath(uuid: UUID): string | undefined { const db = useContext(FileStorageContext); return useLiveQuery(() => db.metadata.get(uuid, (x) => x?.path)); } + +/** + * Gets the file path for a file UUID. + * + * If the file is renamed, the returned value will be automatically updated. + */ +export function useFileStorageUuid(path: string): UUID | undefined { + const db = useContext(FileStorageContext); + return useLiveQuery(() => + db.metadata + .where('path') + .equals(path) + .first((x) => x?.uuid), + ); +} diff --git a/src/hub/sagas.ts b/src/hub/sagas.ts index aa022f017..de9dec30f 100644 --- a/src/hub/sagas.ts +++ b/src/hub/sagas.ts @@ -206,6 +206,16 @@ function* handleDownloadAndRun(action: ReturnType): Gener }); if (didFailToCompile) { + yield* put( + alertsShowAlert( + 'mpy', + 'compilerError', + { + error: didFailToCompile.error, + }, + 'mpy.compilerError', + ), + ); return; } diff --git a/src/mpy/alerts/CompilerError.tsx b/src/mpy/alerts/CompilerError.tsx new file mode 100644 index 000000000..befded906 --- /dev/null +++ b/src/mpy/alerts/CompilerError.tsx @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { Button, Intent } from '@blueprintjs/core'; +import React, { useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { editorGoto } from '../../editor/actions'; +import { useFileStorageUuid } from '../../fileStorage/hooks'; +import type { CreateToast } from '../../toasterTypes'; +import { useI18n } from './i18n'; + +type CompilerErrorProps = { + error: string[]; +}; + +const CompilerError: React.VoidFunctionComponent = ({ error }) => { + const dispatch = useDispatch(); + const i18n = useI18n(); + + const [file, line] = useMemo(() => { + for (const line of error) { + const match = line.match(/^ {2}File "(.*)", line (\d+)/); + + if (match) { + return [match[1], Number(match[2])]; + } + } + + return [undefined, undefined]; + }, [error]); + + const uuid = useFileStorageUuid(file ?? ''); + + return ( + <> +

{i18n.translate('compilerError.message')}

+
{error.join('\n')}
+ {file && uuid && ( + + )} + + ); +}; + +export const compilerError: CreateToast = ( + onAction, + props, +) => { + return { + message: , + icon: 'error', + intent: Intent.DANGER, + timeout: 0, + onDismiss: () => onAction('dismiss'), + }; +}; diff --git a/src/mpy/alerts/i18n.ts b/src/mpy/alerts/i18n.ts new file mode 100644 index 000000000..eb8dc4868 --- /dev/null +++ b/src/mpy/alerts/i18n.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { useI18n as useShopifyI18n } from '@shopify/react-i18n'; +import type { TypedI18n } from '../../i18n'; +import type translations from './translations/en.json'; + +export function useI18n(): TypedI18n { + // istanbul ignore next: babel-loader rewrites this line + const [i18n] = useShopifyI18n(); + return i18n; +} diff --git a/src/mpy/alerts/index.scss b/src/mpy/alerts/index.scss new file mode 100644 index 000000000..b362c37b2 --- /dev/null +++ b/src/mpy/alerts/index.scss @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +pre.pb-mpy-alerts-compile-error { + white-space: pre-wrap; + word-break: keep-all; +} diff --git a/src/mpy/alerts/index.ts b/src/mpy/alerts/index.ts new file mode 100644 index 000000000..465d0c64e --- /dev/null +++ b/src/mpy/alerts/index.ts @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import './index.scss'; +import { compilerError } from './CompilerError'; + +// gathers all of the alert creation functions for passing up to the top level +export default { + compilerError, +}; diff --git a/src/mpy/alerts/translations/en.json b/src/mpy/alerts/translations/en.json new file mode 100644 index 000000000..ef6a42717 --- /dev/null +++ b/src/mpy/alerts/translations/en.json @@ -0,0 +1,6 @@ +{ + "compilerError": { + "message": "Failed to compile MicroPython file.", + "gotoErrorButton": "Goto error" + } +}