From 43ca85a351e5073ad64129ccfc1c6a22603ce4e1 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 26 Mar 2025 22:32:07 +0100 Subject: [PATCH] [red-knot] Add run panel (#17002) ## Summary This PR adds a new secondary panel to the red knot playground that allows running the python code (current file) with [pyodide](https://pyodide.org/en/stable/index.html) (currently Python 3.12 only). ## Test Plan https://github.com/user-attachments/assets/7bda8ef7-19fb-4c2f-8e62-8e49a1416be1 --- playground/knot/package.json | 4 + playground/knot/src/Editor/Chrome.tsx | 10 +- playground/knot/src/Editor/SecondaryPanel.tsx | 150 +++++++++++-- .../knot/src/Editor/SecondarySideBar.tsx | 9 + playground/knot/src/Playground.tsx | 4 +- playground/knot/vite.config.ts | 25 ++- playground/package-lock.json | 209 ++++++++++++++++++ playground/shared/src/Icons.tsx | 25 +++ 8 files changed, 414 insertions(+), 22 deletions(-) diff --git a/playground/knot/package.json b/playground/knot/package.json index 01ee926cfd..1e0001187d 100644 --- a/playground/knot/package.json +++ b/playground/knot/package.json @@ -19,6 +19,7 @@ "classnames": "^2.5.1", "lz-string": "^1.5.0", "monaco-editor": "^0.52.2", + "pyodide": "^0.27.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^2.1.7", @@ -31,5 +32,8 @@ "react": "$react", "react-dom": "$react-dom" } + }, + "devDependencies": { + "vite-plugin-static-copy": "^2.3.0" } } diff --git a/playground/knot/src/Editor/Chrome.tsx b/playground/knot/src/Editor/Chrome.tsx index 969e0e0dd8..ebfadade91 100644 --- a/playground/knot/src/Editor/Chrome.tsx +++ b/playground/knot/src/Editor/Chrome.tsx @@ -12,7 +12,7 @@ import { Theme, VerticalResizeHandle, } from "shared"; -import { Diagnostic, Workspace } from "red_knot_wasm"; +import type { Diagnostic, Workspace } from "red_knot_wasm"; import { Panel, PanelGroup } from "react-resizable-panels"; import { Files } from "./Files"; import SecondarySideBar from "./SecondarySideBar"; @@ -181,6 +181,7 @@ export default function Chrome({ minSize={10} > -
- -
+ ); } function Content({ + files, tool, result, theme, + revision, }: { tool: SecondaryTool; + files: ReadonlyFiles; + revision: number; result: SecondaryPanelResult; theme: Theme; }) { @@ -54,25 +69,124 @@ function Content({ case "Tokens": language = "RustPythonTokens"; break; + + case "Run": + return ; } return ( - +
+ +
); case "error": - return {result.error}; + return ( +
+ {result.error} +
+ ); } } } + +let pyodidePromise: Promise | null = null; + +function Run({ files, theme }: { files: ReadonlyFiles; theme: Theme }) { + if (pyodidePromise == null) { + pyodidePromise = loadPyodide(); + } + + return ( + Loading}> + + + ); +} + +function RunWithPyiodide({ + files, + pyodidePromise, + theme, +}: { + files: ReadonlyFiles; + theme: Theme; + pyodidePromise: Promise; +}) { + const pyodide = use(pyodidePromise); + + const [output, setOutput] = useState(null); + + if (output == null) { + const handleRun = () => { + let stdout = ""; + + pyodide.setStdout({ + batched(output) { + stdout += output + "\n"; + }, + }); + + const main = files.selected == null ? "" : files.contents[files.selected]; + + for (const file of files.index) { + pyodide.FS.writeFile(file.name, files.contents[file.id]); + } + + try { + // Patch up reveal types + pyodide.runPython(` + import builtins + builtins.reveal_type = print`); + + pyodide.runPython(main); + setOutput(stdout); + } catch (e) { + setOutput(`Failed to run Python script: ${e}`); + } + }; + return ( +
+ + + Run... + + +
+ ); + } + return ( +
+      {output}
+    
+ ); +} diff --git a/playground/knot/src/Editor/SecondarySideBar.tsx b/playground/knot/src/Editor/SecondarySideBar.tsx index af82c8b545..a4909d611f 100644 --- a/playground/knot/src/Editor/SecondarySideBar.tsx +++ b/playground/knot/src/Editor/SecondarySideBar.tsx @@ -3,6 +3,7 @@ import { SecondaryTool } from "./SecondaryPanel"; interface Props { selected: SecondaryTool | null; + onSelected(tool: SecondaryTool): void; } @@ -26,6 +27,14 @@ export default function SecondarySideBar({ selected, onSelected }: Props) { > + onSelected(SecondaryTool.Run)} + > + + ); } diff --git a/playground/knot/src/Playground.tsx b/playground/knot/src/Playground.tsx index 5fabe8386b..59e8f81a6d 100644 --- a/playground/knot/src/Playground.tsx +++ b/playground/knot/src/Playground.tsx @@ -11,7 +11,6 @@ import { import { ErrorMessage, Header, setupMonaco, useTheme } from "shared"; import { FileHandle, Workspace } from "red_knot_wasm"; import { persist, persistLocal, restore } from "./Editor/persist"; -import initRedKnot from "../red_knot_wasm"; import { loader } from "@monaco-editor/react"; import knotSchema from "../../../knot.schema.json"; import Chrome, { formatError } from "./Editor/Chrome"; @@ -399,7 +398,8 @@ export interface InitializedPlayground { // Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state. async function startPlayground(): Promise { - await initRedKnot(); + const red_knot = await import("../red_knot_wasm"); + await red_knot.default(); const monaco = await loader.init(); setupMonaco(monaco, { diff --git a/playground/knot/vite.config.ts b/playground/knot/vite.config.ts index 4667d4047f..4011bb6f8d 100644 --- a/playground/knot/vite.config.ts +++ b/playground/knot/vite.config.ts @@ -1,8 +1,31 @@ import { defineConfig } from "vite"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react-swc"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { viteStaticCopy } from "vite-plugin-static-copy"; + +const PYODIDE_EXCLUDE = [ + "!**/*.{md,html}", + "!**/*.d.ts", + "!**/*.whl", + "!**/node_modules", +]; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react(), tailwindcss(), viteStaticCopyPyodide()], + optimizeDeps: { exclude: ["pyodide"] }, }); + +export function viteStaticCopyPyodide() { + const pyodideDir = dirname(fileURLToPath(import.meta.resolve("pyodide"))); + return viteStaticCopy({ + targets: [ + { + src: [join(pyodideDir, "*"), ...PYODIDE_EXCLUDE], + dest: "assets", + }, + ], + }); +} diff --git a/playground/package-lock.json b/playground/package-lock.json index c529d9cfa8..5fa6093de3 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -38,12 +38,16 @@ "classnames": "^2.5.1", "lz-string": "^1.5.0", "monaco-editor": "^0.52.2", + "pyodide": "^0.27.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^2.1.7", "red_knot_wasm": "file:red_knot_wasm", "shared": "0.0.0", "smol-toml": "^1.3.1" + }, + "devDependencies": { + "vite-plugin-static-copy": "^2.3.0" } }, "knot/red_knot_wasm": { @@ -1806,6 +1810,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2014,6 +2032,19 @@ "dev": true, "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/binary-install": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.1.0.tgz", @@ -2130,6 +2161,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -3065,6 +3134,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3522,6 +3606,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -3942,6 +4039,19 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4416,6 +4526,16 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4617,6 +4737,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4774,6 +4907,18 @@ "node": ">=6" } }, + "node_modules/pyodide": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.4.tgz", + "integrity": "sha512-2y3ySHCBmyzYDUlB939SaU3n7RxYQxwnGHgdakW/CPrNFX2L9fC+4nfJWQJH8a0ruQa8bBZSKCImMt/cq15RiQ==", + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4833,6 +4978,19 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/red_knot_wasm": { "resolved": "knot/red_knot_wasm", "link": true @@ -5630,6 +5788,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5712,6 +5880,26 @@ } } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.0.tgz", + "integrity": "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "p-map": "^7.0.3", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, "node_modules/wasm-pack": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.13.1.tgz", @@ -5848,6 +6036,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/playground/shared/src/Icons.tsx b/playground/shared/src/Icons.tsx index 7c9db3956f..4c6c4bbfbb 100644 --- a/playground/shared/src/Icons.tsx +++ b/playground/shared/src/Icons.tsx @@ -46,6 +46,31 @@ export function File({ ); } +export function Run({ + width = 24, + height = 24, +}: { + width?: number; + height?: number; +}) { + return ( + + + + ); +} + export function Settings() { return (