mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 13:15:06 +00:00
Remove all useEffect
usages (#12659)
This commit is contained in:
parent
2daa914334
commit
f53733525c
7 changed files with 208 additions and 168 deletions
3
playground/.gitignore
vendored
3
playground/.gitignore
vendored
|
@ -128,3 +128,6 @@ dist
|
|||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Wrangler
|
||||
api/.wrangler
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
123
playground/src/Editor/Chrome.tsx
Normal file
123
playground/src/Editor/Chrome.tsx
Normal file
|
@ -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 | Promise<void>>(null);
|
||||
const [pythonSource, setPythonSource] = useState<null | string>(null);
|
||||
const [settings, setSettings] = useState<null | string>(null);
|
||||
const [revision, setRevision] = useState(0);
|
||||
const [ruffVersion, setRuffVersion] = useState<string | null>(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 (
|
||||
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
|
||||
<Header
|
||||
edit={revision}
|
||||
theme={theme}
|
||||
version={ruffVersion}
|
||||
onChangeTheme={setTheme}
|
||||
onShare={handleShare}
|
||||
/>
|
||||
|
||||
<div className="flex flex-grow">
|
||||
{source != null && (
|
||||
<Editor
|
||||
theme={theme}
|
||||
source={source}
|
||||
onSettingsChanged={handleSettingsChanged}
|
||||
onSourceChanged={handleSourceChanged}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
};
|
||||
}
|
|
@ -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<string | null>(null);
|
||||
const [checkResult, setCheckResult] = useState<CheckResult>({
|
||||
diagnostics: [],
|
||||
error: null,
|
||||
secondary: null,
|
||||
});
|
||||
const [source, setSource] = useState<Source | null>(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<Tab>("Source");
|
||||
const [secondaryTool, setSecondaryTool] = useState<SecondaryTool | null>(
|
||||
() => {
|
||||
|
@ -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 (
|
||||
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
|
||||
<Header
|
||||
edit={source ? source.revision : null}
|
||||
theme={theme}
|
||||
version={ruffVersion}
|
||||
onChangeTheme={setTheme}
|
||||
onShare={handleShare}
|
||||
/>
|
||||
|
||||
<div className="flex flex-grow">
|
||||
{source ? (
|
||||
<PanelGroup direction="horizontal" autoSaveId="main">
|
||||
<PrimarySideBar
|
||||
onSelectTool={(tool) => setTab(tool)}
|
||||
selected={tab}
|
||||
/>
|
||||
<Panel id="main" order={0} className="my-2" minSize={10}>
|
||||
<SourceEditor
|
||||
visible={tab === "Source"}
|
||||
source={source.pythonSource}
|
||||
<>
|
||||
<PanelGroup direction="horizontal" autoSaveId="main">
|
||||
<PrimarySideBar onSelectTool={(tool) => setTab(tool)} selected={tab} />
|
||||
<Panel id="main" order={0} className="my-2" minSize={10}>
|
||||
<SourceEditor
|
||||
visible={tab === "Source"}
|
||||
source={source.pythonSource}
|
||||
theme={theme}
|
||||
diagnostics={checkResult.diagnostics}
|
||||
onChange={onSourceChanged}
|
||||
/>
|
||||
<SettingsEditor
|
||||
visible={tab === "Settings"}
|
||||
source={source.settingsSource}
|
||||
theme={theme}
|
||||
onChange={onSettingsChanged}
|
||||
/>
|
||||
</Panel>
|
||||
{secondaryTool != null && (
|
||||
<>
|
||||
<HorizontalResizeHandle />
|
||||
<Panel
|
||||
id="secondary-panel"
|
||||
order={1}
|
||||
className={"my-2"}
|
||||
minSize={10}
|
||||
>
|
||||
<SecondaryPanel
|
||||
theme={theme}
|
||||
diagnostics={checkResult.diagnostics}
|
||||
onChange={handlePythonSourceChange}
|
||||
/>
|
||||
<SettingsEditor
|
||||
visible={tab === "Settings"}
|
||||
source={source.settingsSource}
|
||||
theme={theme}
|
||||
onChange={handleSettingsSourceChange}
|
||||
tool={secondaryTool}
|
||||
result={checkResult.secondary}
|
||||
/>
|
||||
</Panel>
|
||||
{secondaryTool != null && (
|
||||
<>
|
||||
<HorizontalResizeHandle />
|
||||
<Panel
|
||||
id="secondary-panel"
|
||||
order={1}
|
||||
className={"my-2"}
|
||||
minSize={10}
|
||||
>
|
||||
<SecondaryPanel
|
||||
theme={theme}
|
||||
tool={secondaryTool}
|
||||
result={checkResult.secondary}
|
||||
/>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
<SecondarySideBar
|
||||
selected={secondaryTool}
|
||||
onSelected={handleSecondaryToolSelected}
|
||||
/>
|
||||
</PanelGroup>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<SecondarySideBar
|
||||
selected={secondaryTool}
|
||||
onSelected={handleSecondaryToolSelected}
|
||||
/>
|
||||
</PanelGroup>
|
||||
|
||||
{checkResult.error && tab === "Source" ? (
|
||||
<div
|
||||
style={{
|
||||
|
@ -283,7 +199,7 @@ export default function Editor() {
|
|||
<ErrorMessage>{checkResult.error}</ErrorMessage>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<Theme>("light");
|
||||
const [localTheme, setLocalTheme] = useState<Theme>(() =>
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<Editor />
|
||||
<Chrome />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue