Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@juggle/resize-observer": "^3.3.1",
"@lezer/highlight": "^1.0.0",
"@raspberrypifoundation/design-system-react": "^2.7.0",
"@raspberrypifoundation/python-friendly-error-messages": "^0.1.6",
"@react-three/drei": "9.114.3",
"@react-three/fiber": "^8.0.13",
"@reduxjs/toolkit": "^1.6.2",
Expand Down
10 changes: 10 additions & 0 deletions src/assets/stylesheets/ErrorMessage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@
@include font-size-2(regular);
}
}

.error-explanation__content {
margin: 0.5rem;
padding: 1rem;
background-color: #f8f8f8;

div {
margin-bottom: 0.5rem;
}
}
53 changes: 50 additions & 3 deletions src/components/Editor/ErrorMessage/ErrorMessage.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,68 @@
import React, { useContext, useEffect, useRef } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";

import "../../../assets/stylesheets/ErrorMessage.scss";

import { useSelector } from "react-redux";

import { SettingsContext } from "../../../utils/settings";

import {
loadCopydeckFor,
registerAdapter,
pyodideAdapter,
friendlyExplain,
} from "@raspberrypifoundation/python-friendly-error-messages";

const ErrorMessage = () => {
const message = useRef();
const errorExplanation = useRef();
const error = useSelector((state) => state.editor.error);
const errorLine = useSelector((state) => state.editor.errorLine);
const code = useSelector((state) => state.editor.code);
console.log("ErrorMessage render", { error: error, code: code });
// TODO: highlight the error line in the code editor
// const errorLineNumber = useSelector((state) => state.editor.errorLineNumber);
const settings = useContext(SettingsContext);
const [isReady, setIsReady] = useState(false);

useEffect(() => {
if (message.current) {
loadCopydeckFor(navigator.language).then(() => {
// TODO: adapt based on runner
// state.editor.activeRunner (and/or loadedRunner) will provide either "pyodide" or "skulpt"
registerAdapter("pyodide", pyodideAdapter);
setIsReady(true);
});
}, []);

useEffect(() => {
if (!message.current || !error || !isReady) return;
try {
const explanation = friendlyExplain({
error: error,
code: code || errorLine,
runtime: "pyodide",
});
const explained = explanation.html || explanation.summary;
message.current.innerHTML = error;
if (explained) {
errorExplanation.current.innerHTML += `${explained}`;
}
} catch {
message.current.innerHTML = error;
}
}, [error]);
}, [error, errorLine, isReady]);

return error ? (
<div className={`error-message error-message--${settings.fontSize}`}>
<pre ref={message} className="error-message__content"></pre>
<div
className={`error-explanation error-explanation--${settings.fontSize}`}
>
<div
ref={errorExplanation}
className="error-explanation__content"
></div>
</div>
</div>
) : null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useTranslation } from "react-i18next";
import classNames from "classnames";
import {
setError,
setErrorLine,
setErrorLineNumber,
codeRunHandled,
setLoadedRunner,
updateProjectComponent,
Expand Down Expand Up @@ -192,6 +194,8 @@ const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => {

const handleError = (file, line, mistake, type, info) => {
let errorMessage;
let errorLine = "";
let errorLineNumber = null;

if (type === "KeyboardInterrupt") {
errorMessage = t("output.errors.interrupted");
Expand All @@ -203,13 +207,34 @@ const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => {
errorMessage += `:\n${mistake}`;
}

if (line && file) {
errorLineNumber = line;
const lastDotIndex = file.lastIndexOf(".");
const fileName =
lastDotIndex > 0 ? file.substring(0, lastDotIndex) : file;
const fileExtension =
lastDotIndex > 0 ? file.substring(lastDotIndex + 1) : "";
const component = projectCode.find(
(item) => item.name === fileName && item.extension === fileExtension,
);
if (component && component.content) {
const lines = component.content.split("\n");
// line numbers are 1-indexed, array is 0-indexed
if (line > 0 && line <= lines.length) {
errorLine = lines[line - 1];
}
}
}

const { createError } = ApiCallHandler({
reactAppApiEndpoint,
});
createError(projectIdentifier, userId, { errorType: type, errorMessage });
}

dispatch(setError(errorMessage));
dispatch(setErrorLine(errorLine));
dispatch(setErrorLineNumber(errorLineNumber));
disableInput();
};

Expand Down Expand Up @@ -256,6 +281,8 @@ const PyodideRunner = ({ active, outputPanels = ["text", "visual"] }) => {
const handleRun = async () => {
output.current.innerHTML = "";
dispatch(setError(""));
dispatch(setErrorLine(""));
dispatch(setErrorLineNumber(null));
setVisuals([]);
stdinClosed.current = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import classNames from "classnames";
import {
setError,
setErrorDetails,
setErrorLine,
setErrorLineNumber,
codeRunHandled,
stopDraw,
setSenseHatEnabled,
Expand Down Expand Up @@ -161,6 +163,8 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
input.removeAttribute("id");
input.removeAttribute("contentEditable");
dispatch(setError(t("output.errors.interrupted")));
dispatch(setErrorLine(""));
dispatch(setErrorLineNumber(null));
dispatch(codeRunHandled());
}
}, [codeRunStopped]);
Expand Down Expand Up @@ -329,6 +333,9 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
let errorDetails = {};
let errorMessage;
let explanation;
let errorLine = "";
let errorLineNumber = null;

if (err.message === t("output.errors.interrupted")) {
errorMessage = err.message;
errorDetails = {
Expand All @@ -343,6 +350,25 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
const lineNumber = err.traceback[0].lineno;
const fileName = err.traceback[0].filename.replace(/^\.\//, "");

if (lineNumber && fileName) {
errorLineNumber = lineNumber;
const lastDotIndex = fileName.lastIndexOf(".");
const name =
lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
const extension =
lastDotIndex > 0 ? fileName.substring(lastDotIndex + 1) : "";
const component = projectCode.find(
(item) => item.name === name && item.extension === extension,
);
if (component && component.content) {
const lines = component.content.split("\n");
// line numbers are 1-indexed, array is 0-indexed
if (lineNumber > 0 && lineNumber <= lines.length) {
errorLine = lines[lineNumber - 1];
}
}
}

if (errorType === "ImportError" && window.crossOriginIsolated) {
const articleLink = `https://help.editor.raspberrypi.org/hc/en-us/articles/30841379339924-What-Python-libraries-are-available-in-the-Code-Editor`;
const moduleName = errorDescription.replace(/No module named /, "");
Expand Down Expand Up @@ -375,6 +401,8 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {

dispatch(setError(errorMessage));
dispatch(setErrorDetails(errorDetails));
dispatch(setErrorLine(errorLine));
dispatch(setErrorLineNumber(errorLineNumber));
dispatch(stopDraw());
if (getInput()) {
const input = getInput();
Expand All @@ -387,6 +415,8 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
// clear previous output
dispatch(setError(""));
dispatch(setErrorDetails({}));
dispatch(setErrorLine(""));
dispatch(setErrorLineNumber(null));
if (output.current) {
output.current.innerHTML = "";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import Sk from "skulpt";
import AstroPiModel from "../../../AstroPiModel/AstroPiModel";
import { codeRunHandled, setError } from "../../../../redux/EditorSlice";
import {
codeRunHandled,
setError,
setErrorLine,
setErrorLineNumber,
} from "../../../../redux/EditorSlice";

const VisualOutputPane = () => {
const codeRunTriggered = useSelector(
Expand Down Expand Up @@ -68,6 +73,8 @@ const VisualOutputPane = () => {

if (error === "") {
dispatch(setError(t("output.errors.interrupted")));
dispatch(setErrorLine(""));
dispatch(setErrorLineNumber(null));
}
dispatch(codeRunHandled());
}
Expand Down
9 changes: 8 additions & 1 deletion src/components/Modals/ErrorModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";

import Button from "../Button/Button";
import { closeErrorModal, setError } from "../../redux/EditorSlice";
import {
closeErrorModal,
setError,
setErrorLine,
setErrorLineNumber,
} from "../../redux/EditorSlice";
import "../../assets/stylesheets/Modal.scss";

const ErrorModal = ({ errorType, additionalOnClose }) => {
Expand All @@ -21,6 +26,8 @@ const ErrorModal = ({ errorType, additionalOnClose }) => {
additionalOnClose();
}
dispatch(setError(null));
dispatch(setErrorLine(""));
dispatch(setErrorLineNumber(null));
};

return (
Expand Down
26 changes: 26 additions & 0 deletions src/redux/EditorSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ export const loadProjectList = createAsyncThunk(
},
);

// Helper function to extract all Python code from components
const getAllPythonCode = (components) => {
if (!components || !Array.isArray(components)) {
return "";
}
return components
.filter((component) => component.extension === "py")
.map((component) => component.content || "")
.join("\n");
};

export const editorInitialState = {
project: {},
cascadeUpdate: false,
Expand Down Expand Up @@ -137,6 +148,9 @@ export const editorInitialState = {
sidebarShowing: true,
modals: {},
errorDetails: {},
errorLine: "",
errorLineNumber: null,
code: "",
runnerBeingLoaded: null | "pyodide" | "skulpt",
};

Expand Down Expand Up @@ -199,6 +213,7 @@ export const EditorSlice = createSlice({
content: action.payload.content || "",
});
state.saving = "idle";
state.code = getAllPythonCode(state.project.components);
},
setPage: (state, action) => {
state.page = action.payload;
Expand Down Expand Up @@ -235,6 +250,7 @@ export const EditorSlice = createSlice({
state.openFiles[firstPanelIndex].push("main.py");
}
state.justLoaded = true;
state.code = getAllPythonCode(state.project.components);
},
setProjectInstructions: (state, action) => {
state.project.instructions = action.payload;
Expand Down Expand Up @@ -283,6 +299,7 @@ export const EditorSlice = createSlice({
});
state.project.components = mapped;
state.cascadeUpdate = cascadeUpdate;
state.code = getAllPythonCode(state.project.components);
},
updateProjectName: (state, action) => {
state.project.name = action.payload;
Expand All @@ -303,13 +320,20 @@ export const EditorSlice = createSlice({
state.openFiles[panelIndex][fileIndex] = `${name}.${extension}`;
}
state.saving = "idle";
state.code = getAllPythonCode(state.project.components);
},
setCascadeUpdate: (state, action) => {
state.cascadeUpdate = action.payload;
},
setError: (state, action) => {
state.error = action.payload;
},
setErrorLine: (state, action) => {
state.errorLine = action.payload;
},
setErrorLineNumber: (state, action) => {
state.errorLineNumber = action.payload;
},
triggerCodeRun: (state) => {
state.codeRunTriggered = true;
state.codeHasBeenRun = true;
Expand Down Expand Up @@ -452,6 +476,8 @@ export const {
setBrowserPreview,
setCascadeUpdate,
setError,
setErrorLine,
setErrorLineNumber,
setIsSplitView,
setNameError,
setHasShownSavePrompt,
Expand Down
9 changes: 8 additions & 1 deletion src/utils/externalLinkHelper.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useState } from "react";
import { useDispatch } from "react-redux";

import { setError, triggerCodeRun } from "../redux/EditorSlice";
import {
setError,
setErrorLine,
setErrorLineNumber,
triggerCodeRun,
} from "../redux/EditorSlice";

const domain = "https://rpf.io/";
const host = process.env.PUBLIC_URL || "http://localhost:3011";
Expand All @@ -27,6 +32,8 @@ const useExternalLinkState = (showModal) => {

const handleExternalLinkError = () => {
dispatch(setError("externalLink"));
dispatch(setErrorLine(""));
dispatch(setErrorLineNumber(null));
showModal();
};

Expand Down
6 changes: 6 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ module.exports = {
],
},
resolve: {
alias: {
"@raspberrypifoundation/python-friendly-error-messages": path.resolve(
__dirname,
"node_modules/@raspberrypifoundation/python-friendly-error-messages/dist/index.browser.js",
),
},
extensions: [".*", ".js", ".jsx", ".css"],
fallback: {
stream: require.resolve("stream-browserify"),
Expand Down
Loading