diff --git a/playground/.gitignore b/playground/.gitignore index c6bba59138..e30d2eac55 100644 --- a/playground/.gitignore +++ b/playground/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Wrangler +api/.wrangler diff --git a/playground/README.md b/playground/README.md index 082b7a9f15..3b7c0f394a 100644 --- a/playground/README.md +++ b/playground/README.md @@ -12,7 +12,7 @@ Finally, install TypeScript dependencies with `npm install`, and run the develop To run the datastore, which is based on [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/), install the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/), -then run `npx wrangler dev --local` from the `./playground/db` directory. Note that the datastore is +then run `npx wrangler dev --local` from the `./playground/api` directory. Note that the datastore is only required to generate shareable URLs for code snippets. The development datastore does not require Cloudflare authentication or login, but in turn only persists data locally. diff --git a/playground/src/Editor/Chrome.tsx b/playground/src/Editor/Chrome.tsx new file mode 100644 index 0000000000..b97a1b5f00 --- /dev/null +++ b/playground/src/Editor/Chrome.tsx @@ -0,0 +1,123 @@ +import { useCallback, useMemo, useRef, useState } from "react"; +import Header from "./Header"; +import { persist, persistLocal, restore, stringify } from "./settings"; +import { useTheme } from "./theme"; +import { default as Editor, Source } from "./Editor"; +import initRuff, { Workspace } from "../pkg/ruff_wasm"; +import { loader } from "@monaco-editor/react"; +import { setupMonaco } from "./setupMonaco"; +import { DEFAULT_PYTHON_SOURCE } from "../constants"; + +export default function Chrome() { + const initPromise = useRef>(null); + const [pythonSource, setPythonSource] = useState(null); + const [settings, setSettings] = useState(null); + const [revision, setRevision] = useState(0); + const [ruffVersion, setRuffVersion] = useState(null); + + const [theme, setTheme] = useTheme(); + + const handleShare = useCallback(() => { + if (settings == null || pythonSource == null) { + return; + } + + persist(settings, pythonSource).catch((error) => + console.error(`Failed to share playground: ${error}`), + ); + }, [pythonSource, settings]); + + if (initPromise.current == null) { + initPromise.current = startPlayground() + .then(({ sourceCode, settings, ruffVersion }) => { + setPythonSource(sourceCode); + setSettings(settings); + setRuffVersion(ruffVersion); + setRevision(1); + }) + .catch((error) => { + console.error("Failed to initialize playground.", error); + }); + } + + const handleSourceChanged = useCallback( + (source: string) => { + setPythonSource(source); + setRevision((revision) => revision + 1); + + if (settings != null) { + persistLocal({ pythonSource: source, settingsSource: settings }); + } + }, + [settings], + ); + + const handleSettingsChanged = useCallback( + (settings: string) => { + setSettings(settings); + setRevision((revision) => revision + 1); + + if (pythonSource != null) { + persistLocal({ pythonSource: pythonSource, settingsSource: settings }); + } + }, + [pythonSource], + ); + + const source: Source | null = useMemo(() => { + if (pythonSource == null || settings == null) { + return null; + } + + return { pythonSource, settingsSource: settings }; + }, [settings, pythonSource]); + + return ( +
+
+ +
+ {source != null && ( + + )} +
+
+ ); +} + +// Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state. +async function startPlayground(): Promise<{ + sourceCode: string; + settings: string; + ruffVersion: string; +}> { + await initRuff(); + const monaco = await loader.init(); + + console.log(monaco); + + setupMonaco(monaco); + + const response = await restore(); + const [settingsSource, pythonSource] = response ?? [ + stringify(Workspace.defaultSettings()), + DEFAULT_PYTHON_SOURCE, + ]; + + return { + sourceCode: pythonSource, + settings: settingsSource, + ruffVersion: Workspace.version(), + }; +} diff --git a/playground/src/Editor/Editor.tsx b/playground/src/Editor/Editor.tsx index 5bfc5bc1cf..1320fb4b20 100644 --- a/playground/src/Editor/Editor.tsx +++ b/playground/src/Editor/Editor.tsx @@ -1,15 +1,7 @@ -import { - useCallback, - useDeferredValue, - useEffect, - useMemo, - useState, -} from "react"; +import { useDeferredValue, useMemo, useState } from "react"; import { Panel, PanelGroup } from "react-resizable-panels"; -import { DEFAULT_PYTHON_SOURCE } from "../constants"; -import init, { Diagnostic, Workspace } from "../pkg/ruff_wasm"; +import { Diagnostic, Workspace } from "../pkg/ruff_wasm"; import { ErrorMessage } from "./ErrorMessage"; -import Header from "./Header"; import PrimarySideBar from "./PrimarySideBar"; import { HorizontalResizeHandle } from "./ResizeHandle"; import SecondaryPanel, { @@ -17,17 +9,15 @@ import SecondaryPanel, { SecondaryTool, } from "./SecondaryPanel"; import SecondarySideBar from "./SecondarySideBar"; -import { persist, persistLocal, restore, stringify } from "./settings"; import SettingsEditor from "./SettingsEditor"; import SourceEditor from "./SourceEditor"; -import { useTheme } from "./theme"; +import { Theme } from "./theme"; type Tab = "Source" | "Settings"; -interface Source { +export interface Source { pythonSource: string; settingsSource: string; - revision: number; } interface CheckResult { @@ -36,15 +26,20 @@ interface CheckResult { secondary: SecondaryPanelResult; } -export default function Editor() { - const [ruffVersion, setRuffVersion] = useState(null); - const [checkResult, setCheckResult] = useState({ - diagnostics: [], - error: null, - secondary: null, - }); - const [source, setSource] = useState(null); +type Props = { + source: Source; + theme: Theme; + onSourceChanged(source: string): void; + onSettingsChanged(settings: string): void; +}; + +export default function Editor({ + source, + theme, + onSourceChanged, + onSettingsChanged, +}: Props) { const [tab, setTab] = useState("Source"); const [secondaryTool, setSecondaryTool] = useState( () => { @@ -58,7 +53,6 @@ export default function Editor() { } }, ); - const [theme, setTheme] = useTheme(); // Ideally this would be retrieved right from the URL... but routing without a proper // router is hard (there's no location changed event) and pulling in a router @@ -81,33 +75,9 @@ export default function Editor() { setSecondaryTool(tool); }; - useEffect(() => { - async function initAsync() { - await init(); - const response = await restore(); - const [settingsSource, pythonSource] = response ?? [ - stringify(Workspace.defaultSettings()), - DEFAULT_PYTHON_SOURCE, - ]; - - setSource({ - revision: 0, - pythonSource, - settingsSource, - }); - setRuffVersion(Workspace.version()); - } - - initAsync().catch(console.error); - }, []); - const deferredSource = useDeferredValue(source); - useEffect(() => { - if (deferredSource == null) { - return; - } - + const checkResult: CheckResult = useMemo(() => { const { pythonSource, settingsSource } = deferredSource; try { @@ -161,116 +131,62 @@ export default function Editor() { }; } - setCheckResult({ + return { diagnostics, error: null, secondary, - }); + }; } catch (e) { - setCheckResult({ + return { diagnostics: [], error: (e as Error).message, secondary: null, - }); + }; } }, [deferredSource, secondaryTool]); - useEffect(() => { - if (source != null) { - persistLocal(source); - } - }, [source]); - - const handleShare = useMemo(() => { - if (source == null) { - return undefined; - } - - return () => { - return persist(source.settingsSource, source.pythonSource); - }; - }, [source]); - - const handlePythonSourceChange = useCallback((pythonSource: string) => { - setSource((state) => - state - ? { - ...state, - pythonSource, - revision: state.revision + 1, - } - : null, - ); - }, []); - - const handleSettingsSourceChange = useCallback((settingsSource: string) => { - setSource((state) => - state - ? { - ...state, - settingsSource, - revision: state.revision + 1, - } - : null, - ); - }, []); - return ( -
-
- -
- {source ? ( - - setTab(tool)} - selected={tab} - /> - - + + setTab(tool)} selected={tab} /> + + + + + {secondaryTool != null && ( + <> + + + - - {secondaryTool != null && ( - <> - - - - - - )} - - - ) : null} -
+ + )} + + + {checkResult.error && tab === "Source" ? (
{checkResult.error}
) : null} -
+ ); } diff --git a/playground/src/Editor/SourceEditor.tsx b/playground/src/Editor/SourceEditor.tsx index 50d14f7447..c74946e59b 100644 --- a/playground/src/Editor/SourceEditor.tsx +++ b/playground/src/Editor/SourceEditor.tsx @@ -5,7 +5,7 @@ import Editor, { BeforeMount, Monaco } from "@monaco-editor/react"; import { MarkerSeverity, MarkerTag } from "monaco-editor"; import { useCallback, useEffect, useRef } from "react"; -import { Diagnostic } from "../pkg"; +import { Diagnostic } from "../pkg/ruff_wasm"; import { Theme } from "./theme"; export default function SourceEditor({ diff --git a/playground/src/Editor/theme.ts b/playground/src/Editor/theme.ts index 60446a800f..7549dae586 100644 --- a/playground/src/Editor/theme.ts +++ b/playground/src/Editor/theme.ts @@ -1,12 +1,14 @@ /** * Light and dark mode theming. */ -import { useEffect, useState } from "react"; +import { useState } from "react"; export type Theme = "dark" | "light"; export function useTheme(): [Theme, (theme: Theme) => void] { - const [localTheme, setLocalTheme] = useState("light"); + const [localTheme, setLocalTheme] = useState(() => + detectInitialTheme(), + ); const setTheme = (mode: Theme) => { if (mode === "dark") { @@ -18,18 +20,18 @@ export function useTheme(): [Theme, (theme: Theme) => void] { setLocalTheme(mode); }; - useEffect(() => { - const initialTheme = localStorage.getItem("theme"); - if (initialTheme === "dark") { - setTheme("dark"); - } else if (initialTheme === "light") { - setTheme("light"); - } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - setTheme("dark"); - } else { - setTheme("light"); - } - }, []); - return [localTheme, setTheme]; } + +function detectInitialTheme(): Theme { + const initialTheme = localStorage.getItem("theme"); + if (initialTheme === "dark") { + return "dark"; + } else if (initialTheme === "light") { + return "light"; + } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } else { + return "light"; + } +} diff --git a/playground/src/main.tsx b/playground/src/main.tsx index d62cb07f57..fbe0181a4d 100644 --- a/playground/src/main.tsx +++ b/playground/src/main.tsx @@ -1,14 +1,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import Editor from "./Editor"; import "./index.css"; -import { loader } from "@monaco-editor/react"; -import { setupMonaco } from "./Editor/setupMonaco"; - -loader.init().then(setupMonaco); +import Chrome from "./Editor/Chrome"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + , );