diff --git a/.env.example b/.env.example
index 4d4e2433a..1a05d8f42 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,4 @@
+EDITOR_STANDALONE_URL='http://localhost:3012'
REACT_APP_AUTHENTICATION_CLIENT_ID='editor-dev'
REACT_APP_SENTRY_DSN=''
REACT_APP_SENTRY_ENV='local'
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index de3b9fe93..8a88e3579 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -18,6 +18,10 @@ on:
required: false
default: "https://staging-editor-static.raspberrypi.org"
type: string
+ editor_standalone_url:
+ required: false
+ default: "https://ipython-spike.editor-standalone-staging.pages.dev"
+ type: string
react_app_api_endpoint:
required: false
default: "https://staging-editor-api.raspberrypi.org"
@@ -133,6 +137,7 @@ jobs:
env:
PUBLIC_URL: ${{ needs.setup-environment.outputs.public_url }}
ASSETS_URL: ${{ needs.setup-environment.outputs.assets_url }}
+ EDITOR_STANDALONE_URL: ${{ inputs.editor_standalone_url }}
REACT_APP_API_ENDPOINT: ${{ inputs.react_app_api_endpoint }}
REACT_APP_AUTHENTICATION_CLIENT_ID: ${{ inputs.react_app_authentication_client_id }}
REACT_APP_AUTHENTICATION_URL: ${{ inputs.react_app_authentication_url }}
diff --git a/src/assets/stylesheets/PythonRunner.scss b/src/assets/stylesheets/PythonRunner.scss
index 66b7449b8..1430f113b 100644
--- a/src/assets/stylesheets/PythonRunner.scss
+++ b/src/assets/stylesheets/PythonRunner.scss
@@ -17,7 +17,6 @@
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
- width: 100%;
word-wrap: break-word;
overflow-y: auto;
@@ -41,7 +40,6 @@
.pythonrunner-input {
caret-color: inherit;
color: rgb(36, 103, 236);
- display: block;
line-height: 20px;
padding: 2px 1px 2px 0;
}
diff --git a/src/components/Editor/Output/Output.jsx b/src/components/Editor/Output/Output.jsx
index ba55119bc..824c330ac 100644
--- a/src/components/Editor/Output/Output.jsx
+++ b/src/components/Editor/Output/Output.jsx
@@ -4,11 +4,18 @@ import ExternalFiles from "../../ExternalFiles/ExternalFiles";
import RunnerFactory from "../Runners/RunnerFactory";
import RunBar from "../../RunButton/RunBar";
-const Output = ({ outputPanels = ["text", "visual"] }) => {
+const Output = ({
+ outputPanels = ["text", "visual"],
+ autoRun = false,
+ showOutputTabs = true,
+}) => {
const project = useSelector((state) => state.editor.project);
const isEmbedded = useSelector((state) => state.editor.isEmbedded);
const searchParams = new URLSearchParams(window.location.search);
const isBrowserPreview = searchParams.get("browserPreview") === "true";
+ const isAutoRun = autoRun || searchParams.get("autoRun") === "true";
+ const shouldShowOutputTabs =
+ showOutputTabs && searchParams.get("showOutputTabs") !== "false";
return (
<>
@@ -17,6 +24,8 @@ const Output = ({ outputPanels = ["text", "visual"] }) => {
{isEmbedded && !isBrowserPreview && }
diff --git a/src/components/Editor/Project/Project.jsx b/src/components/Editor/Project/Project.jsx
index 4fa543b97..7e7fa6992 100644
--- a/src/components/Editor/Project/Project.jsx
+++ b/src/components/Editor/Project/Project.jsx
@@ -18,6 +18,7 @@ import { projContainer } from "../../../utils/containerQueries";
const Project = (props) => {
const webComponent = useSelector((state) => state.editor.webComponent);
const {
+ autoRun = false,
nameEditable = true,
withProjectbar = true,
withSidebar = true,
@@ -75,7 +76,7 @@ const Project = (props) => {
>
-
+
)}
diff --git a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx
index 157451968..bd36ea985 100644
--- a/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx
+++ b/src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.jsx
@@ -7,6 +7,7 @@ import classNames from "classnames";
import {
setError,
codeRunHandled,
+ triggerCodeRun,
setLoadedRunner,
} from "../../../../../redux/EditorSlice";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
@@ -18,6 +19,11 @@ import VisualOutputPane from "./VisualOutputPane";
import OutputViewToggle from "../OutputViewToggle";
import { SettingsContext } from "../../../../../utils/settings";
import RunnerControls from "../../../../RunButton/RunnerControls";
+import { useCookies } from "react-cookie";
+// import "prismjs/plugins/highlight-keywords/prism-highlight-keywords.js";
+// import Prism from "prismjs";
+
+// window.Prism = Prism;
const getWorkerURL = (url) => {
const content = `
@@ -31,7 +37,12 @@ const getWorkerURL = (url) => {
return URL.createObjectURL(blob);
};
-const PyodideRunner = ({ active }) => {
+const PyodideRunner = ({
+ active,
+ consoleMode = false,
+ autoRun = false,
+ showOutputTabs = true,
+}) => {
const [pyodideWorker, setPyodideWorker] = useState(null);
useEffect(() => {
@@ -64,10 +75,110 @@ const PyodideRunner = ({ active }) => {
const settings = useContext(SettingsContext);
const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });
const senseHatAlways = useSelector((s) => s.editor.senseHatAlwaysEnabled);
+ const isOutputOnly = useSelector((state) => state.editor.isOutputOnly);
const queryParams = new URLSearchParams(window.location.search);
const showVisualTab = queryParams.get("show_visual_tab") === "true";
const [hasVisual, setHasVisual] = useState(showVisualTab || senseHatAlways);
const [visuals, setVisuals] = useState([]);
+ const [inputStack, setInputStack] = useState([]);
+ const [indentationLevel, setIndentationLevel] = useState(0);
+ const [awaitingInput, setAwaitingInput] = useState(false);
+ const [cookies] = useCookies(["theme"]);
+ const defaultTheme = window.matchMedia("(prefers-color-scheme:dark)").matches
+ ? "dark"
+ : "light";
+ const theme = cookies.theme || defaultTheme;
+
+ const prependToInputStack = (input) => {
+ setInputStack((prevInputStack) => {
+ if (prevInputStack[0] === "") {
+ const newStack = [...prevInputStack];
+ newStack[0] = input;
+ return newStack;
+ } else {
+ return [input, ...prevInputStack];
+ }
+ });
+ };
+ const [inputStackIndex, setInputStackIndex] = useState(0);
+
+ const incrementIndentationLevel = (prevLine) => {
+ // console.log("prevLine", prevLine);
+ // console.log(prevLine.match(/^\s*/)[0].length);
+ const prevLevel = prevLine
+ ? Math.floor(prevLine.match(/^\s*/)[0].length / 4)
+ : 0;
+ setIndentationLevel(prevLevel + 1);
+ };
+
+ const keepSameIndentationLevel = (prevLine) => {
+ const prevLevel = prevLine
+ ? Math.floor(prevLine.match(/^\s*/)[0].length / 4)
+ : 0;
+ setIndentationLevel(prevLevel);
+ };
+
+ const handleIndentationLevel = (prevLine) => {
+ if (prevLine.trimEnd().slice(-1) === ":") {
+ incrementIndentationLevel(prevLine);
+ } else if (prevLine.trimEnd() === "") {
+ setIndentationLevel(0);
+ } else {
+ keepSameIndentationLevel(prevLine);
+ }
+ };
+
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (event.key === "ArrowUp") {
+ if (inputStackIndex < inputStack.length - 1) {
+ setInputStackIndex(inputStackIndex + 1);
+ }
+ } else if (event.key === "ArrowDown") {
+ if (inputStackIndex > 0) {
+ setInputStackIndex(inputStackIndex - 1);
+ }
+ }
+ // window.Prism.highlightElement(event.target);
+ };
+ if (consoleMode) {
+ const inputElement = getInputElement();
+ inputElement?.removeEventListener("keydown", handleKeyDown);
+ inputElement?.addEventListener("keydown", handleKeyDown);
+ }
+ }, [inputStack, inputStackIndex, consoleMode]);
+
+ useEffect(() => {
+ if (awaitingInput && consoleMode) {
+ const inputElement = getInputElement();
+ const inputParent = inputElement?.parentElement;
+ // inputElement.classList.add("language-python");
+ if (inputParent.innerText.match(/...:/)) {
+ inputElement.innerText = " ".repeat(indentationLevel * 4);
+ // move cursor to end of text
+ const range = document.createRange();
+ const selection = window.getSelection();
+ range.selectNodeContents(inputElement);
+ range.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+ }, [awaitingInput, indentationLevel, consoleMode]);
+
+ useEffect(() => {
+ const inputElement = getInputElement();
+ if (inputElement) {
+ inputElement.innerText = inputStack[inputStackIndex];
+ // move cursor to end of text
+ const range = document.createRange();
+ const selection = window.getSelection();
+ range.selectNodeContents(inputElement);
+ range.collapse(false);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }, [inputStackIndex]);
useEffect(() => {
if (pyodideWorker) {
@@ -107,8 +218,15 @@ const PyodideRunner = ({ active }) => {
}
}, [pyodideWorker]);
+ useEffect(() => {
+ if (autoRun && active && pyodideWorker) {
+ dispatch(triggerCodeRun());
+ }
+ }, [active, pyodideWorker]);
+
useEffect(() => {
if (codeRunTriggered && active && output.current) {
+ console.log("running with pyodide");
handleRun();
}
}, [codeRunTriggered, output.current]);
@@ -141,11 +259,22 @@ const PyodideRunner = ({ active }) => {
return;
}
+ prependToInputStack("");
+ setInputStackIndex(0);
const outputPane = output.current;
- outputPane.appendChild(inputSpan());
+ // remove last new line character from last line
+ outputPane.lastChild.innerText = outputPane.lastChild.innerText.slice(
+ 0,
+ -1,
+ );
+ outputPane.lastChild.appendChild(inputSpan());
const element = getInputElement();
const { content, ctrlD } = await getInputContent(element);
+ setAwaitingInput(false);
+
+ prependToInputStack(content);
+ handleIndentationLevel(content);
const encoder = new TextEncoder();
const bytes = encoder.encode(content + "\n");
@@ -250,6 +379,7 @@ const PyodideRunner = ({ active }) => {
span.setAttribute("spellCheck", "false");
span.setAttribute("class", "pythonrunner-input");
span.setAttribute("contentEditable", "true");
+ setAwaitingInput(true);
return span;
};
@@ -312,6 +442,7 @@ const PyodideRunner = ({ active }) => {
if (element) {
element.removeAttribute("id");
element.removeAttribute("contentEditable");
+ element.addEventListener("keydown");
}
};
@@ -331,7 +462,10 @@ const PyodideRunner = ({ active }) => {
{hasVisual && (
-
+
@@ -350,14 +484,23 @@ const PyodideRunner = ({ active }) => {
)}
-
+
{t("output.textOutput")}
+ {!isOutputOnly && (
+
+ Console
+
+ )}
+
{!hasVisual && !isEmbedded && isMobile && (
)}
@@ -370,12 +513,25 @@ const PyodideRunner = ({ active }) => {
ref={output}
>
+ {!isOutputOnly && (
+
+
+
+ )}
>
) : (
-
+
{hasVisual && (
@@ -389,10 +545,16 @@ const PyodideRunner = ({ active }) => {
{t("output.textOutput")}
+ {/* {!isOutputOnly && (
+
+ Console
+
+ )} */}
{!isEmbedded && hasVisual && }
{!isEmbedded && isMobile && }
+
{hasVisual && (
@@ -406,6 +568,20 @@ const PyodideRunner = ({ active }) => {
ref={output}
>
+ {/* {!isOutputOnly && (
+
+
+
+ )} */}
)}
diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx
index cc34e65cb..3bce862a7 100644
--- a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx
+++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx
@@ -15,9 +15,8 @@ const SKULPT_ONLY_MODULES = [
"turtle",
];
-const PythonRunner = () => {
+const PythonRunner = ({ autoRun = false, showOutputTabs = true }) => {
const dispatch = useDispatch();
-
const project = useSelector((state) => state.editor.project);
const activeRunner = useSelector((state) => state.editor.activeRunner);
const codeRunTriggered = useSelector(
@@ -28,6 +27,7 @@ const PythonRunner = () => {
);
const [usePyodide, setUsePyodide] = useState(null);
const [skulptFallback, setSkulptFallback] = useState(false);
+ const [consoleMode, setConsoleMode] = useState(false);
const { t } = useTranslation();
useEffect(() => {
@@ -83,6 +83,12 @@ const PythonRunner = () => {
break;
} else {
setUsePyodide(true);
+ if (imports.includes("IPython")) {
+ setConsoleMode(true);
+ break;
+ } else {
+ setConsoleMode(false);
+ }
}
} catch (error) {
console.error("Error occurred while getting imports:", error);
@@ -92,7 +98,12 @@ const PythonRunner = () => {
}, [project, codeRunTriggered, senseHatAlwaysEnabled, skulptFallback, t]);
return (
<>
-
+
>
);
diff --git a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx
index 51eee57c9..19d13a381 100644
--- a/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx
+++ b/src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.jsx
@@ -275,7 +275,7 @@ const SkulptRunner = ({ active, outputPanels = ["text", "visual"] }) => {
Sk.sense_hat.mz_criteria.noInputEvents = false;
}
const outputPane = output.current;
- outputPane.appendChild(inputSpan());
+ outputPane.lastChild.appendChild(inputSpan());
const input = getInput();
input.focus();
diff --git a/src/components/Editor/Runners/RunnerFactory.jsx b/src/components/Editor/Runners/RunnerFactory.jsx
index 37f7f8b6f..89831c330 100644
--- a/src/components/Editor/Runners/RunnerFactory.jsx
+++ b/src/components/Editor/Runners/RunnerFactory.jsx
@@ -2,7 +2,12 @@ import React from "react";
import PythonRunner from "./PythonRunner/PythonRunner";
import HtmlRunner from "./HtmlRunner/HtmlRunner";
-const RunnerFactory = ({ projectType, outputPanels = ["text", "visual"] }) => {
+const RunnerFactory = ({
+ autoRun = false,
+ projectType,
+ outputPanels = ["text", "visual"],
+ showOutputTabs = true,
+}) => {
const Runner = () => {
if (projectType === "html") {
return HtmlRunner;
@@ -13,7 +18,8 @@ const RunnerFactory = ({ projectType, outputPanels = ["text", "visual"] }) => {
const Selected = Runner();
- const props = projectType === "html" ? {} : { outputPanels };
+ const props =
+ projectType === "html" ? {} : { outputPanels, autoRun, showOutputTabs };
return ;
};
diff --git a/src/components/Mobile/MobileProject/MobileProject.jsx b/src/components/Mobile/MobileProject/MobileProject.jsx
index 50cc22a65..f4cab0aa3 100644
--- a/src/components/Mobile/MobileProject/MobileProject.jsx
+++ b/src/components/Mobile/MobileProject/MobileProject.jsx
@@ -14,7 +14,11 @@ import { useTranslation } from "react-i18next";
import Sidebar from "../../Menus/Sidebar/Sidebar";
import { showSidebar } from "../../../redux/EditorSlice";
-const MobileProject = ({ withSidebar, sidebarOptions = [] }) => {
+const MobileProject = ({
+ autoRun = false,
+ withSidebar,
+ sidebarOptions = [],
+}) => {
const projectType = useSelector((state) => state.editor.project.project_type);
const sidebarShowing = useSelector((state) => state.editor.sidebarShowing);
const codeRunTriggered = useSelector(
@@ -55,7 +59,7 @@ const MobileProject = ({ withSidebar, sidebarOptions = [] }) => {
-
+
diff --git a/src/components/WebComponentProject/WebComponentProject.jsx b/src/components/WebComponentProject/WebComponentProject.jsx
index 08fa2eb55..29997dd79 100644
--- a/src/components/WebComponentProject/WebComponentProject.jsx
+++ b/src/components/WebComponentProject/WebComponentProject.jsx
@@ -24,6 +24,7 @@ import {
} from "../../events/WebComponentCustomEvents";
const WebComponentProject = ({
+ autoRun = false,
withProjectbar = false,
nameEditable = false,
withSidebar = false,
@@ -31,6 +32,7 @@ const WebComponentProject = ({
outputOnly = false,
outputPanels = ["text", "visual"],
outputSplitView = false,
+ showOutputTabs = true,
}) => {
const loading = useSelector((state) => state.editor.loading);
const project = useSelector((state) => state.editor.project);
@@ -97,11 +99,14 @@ const WebComponentProject = ({
{!outputOnly &&
(isMobile ? (
) : (
- {loading === "success" && }
+ {loading === "success" && (
+
+ )}
)}
>
diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx
index c99422648..581a7c354 100644
--- a/src/containers/WebComponentLoader.jsx
+++ b/src/containers/WebComponentLoader.jsx
@@ -32,6 +32,7 @@ const WebComponentLoader = (props) => {
const {
assetsIdentifier,
authKey,
+ autoRun = false,
code,
embedded = false,
hostStyles, // Pass in styles from the host
@@ -45,6 +46,7 @@ const WebComponentLoader = (props) => {
reactAppApiEndpoint = process.env.REACT_APP_API_ENDPOINT,
readOnly = false,
senseHatAlwaysEnabled = false,
+ showOutputTabs = true,
showSavePrompt = false,
sidebarOptions = [],
theme,
@@ -183,6 +185,7 @@ const WebComponentLoader = (props) => {
closeButton={ToastCloseButton}
/>
{
outputOnly={outputOnly}
outputPanels={outputPanels}
outputSplitView={outputSplitView}
+ showOutputTabs={showOutputTabs}
/>
{errorModalShowing && }
{newFileModalShowing && }
diff --git a/src/web-component.js b/src/web-component.js
index 9a8006752..b962cc0c7 100644
--- a/src/web-component.js
+++ b/src/web-component.js
@@ -48,6 +48,7 @@ class WebComponent extends HTMLElement {
return [
"assets_identifier",
"auth_key",
+ "auto_run",
"code",
"embedded",
"host_styles",
@@ -61,6 +62,7 @@ class WebComponent extends HTMLElement {
"react_app_api_endpoint",
"read_only",
"sense_hat_always_enabled",
+ "show_output_tabs",
"show_save_prompt",
"sidebar_options",
"theme",
@@ -75,6 +77,7 @@ class WebComponent extends HTMLElement {
if (
[
+ "auto_run",
"embedded",
"load_remix_disabled",
"output_only",
@@ -82,6 +85,7 @@ class WebComponent extends HTMLElement {
"project_name_editable",
"read_only",
"sense_hat_always_enabled",
+ "show_output_tabs",
"show_save_prompt",
"use_editor_styles",
"with_projectbar",