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 && ( + +