mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-07 21:25:08 +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/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.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/),
|
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/),
|
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
|
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.
|
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 {
|
import { useDeferredValue, useMemo, useState } from "react";
|
||||||
useCallback,
|
|
||||||
useDeferredValue,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Panel, PanelGroup } from "react-resizable-panels";
|
import { Panel, PanelGroup } from "react-resizable-panels";
|
||||||
import { DEFAULT_PYTHON_SOURCE } from "../constants";
|
import { Diagnostic, Workspace } from "../pkg/ruff_wasm";
|
||||||
import init, { Diagnostic, Workspace } from "../pkg/ruff_wasm";
|
|
||||||
import { ErrorMessage } from "./ErrorMessage";
|
import { ErrorMessage } from "./ErrorMessage";
|
||||||
import Header from "./Header";
|
|
||||||
import PrimarySideBar from "./PrimarySideBar";
|
import PrimarySideBar from "./PrimarySideBar";
|
||||||
import { HorizontalResizeHandle } from "./ResizeHandle";
|
import { HorizontalResizeHandle } from "./ResizeHandle";
|
||||||
import SecondaryPanel, {
|
import SecondaryPanel, {
|
||||||
|
@ -17,17 +9,15 @@ import SecondaryPanel, {
|
||||||
SecondaryTool,
|
SecondaryTool,
|
||||||
} from "./SecondaryPanel";
|
} from "./SecondaryPanel";
|
||||||
import SecondarySideBar from "./SecondarySideBar";
|
import SecondarySideBar from "./SecondarySideBar";
|
||||||
import { persist, persistLocal, restore, stringify } from "./settings";
|
|
||||||
import SettingsEditor from "./SettingsEditor";
|
import SettingsEditor from "./SettingsEditor";
|
||||||
import SourceEditor from "./SourceEditor";
|
import SourceEditor from "./SourceEditor";
|
||||||
import { useTheme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
|
|
||||||
type Tab = "Source" | "Settings";
|
type Tab = "Source" | "Settings";
|
||||||
|
|
||||||
interface Source {
|
export interface Source {
|
||||||
pythonSource: string;
|
pythonSource: string;
|
||||||
settingsSource: string;
|
settingsSource: string;
|
||||||
revision: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckResult {
|
interface CheckResult {
|
||||||
|
@ -36,15 +26,20 @@ interface CheckResult {
|
||||||
secondary: SecondaryPanelResult;
|
secondary: SecondaryPanelResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Editor() {
|
type Props = {
|
||||||
const [ruffVersion, setRuffVersion] = useState<string | null>(null);
|
source: Source;
|
||||||
const [checkResult, setCheckResult] = useState<CheckResult>({
|
theme: Theme;
|
||||||
diagnostics: [],
|
|
||||||
error: null,
|
|
||||||
secondary: null,
|
|
||||||
});
|
|
||||||
const [source, setSource] = useState<Source | null>(null);
|
|
||||||
|
|
||||||
|
onSourceChanged(source: string): void;
|
||||||
|
onSettingsChanged(settings: string): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Editor({
|
||||||
|
source,
|
||||||
|
theme,
|
||||||
|
onSourceChanged,
|
||||||
|
onSettingsChanged,
|
||||||
|
}: Props) {
|
||||||
const [tab, setTab] = useState<Tab>("Source");
|
const [tab, setTab] = useState<Tab>("Source");
|
||||||
const [secondaryTool, setSecondaryTool] = useState<SecondaryTool | null>(
|
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
|
// 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
|
// router is hard (there's no location changed event) and pulling in a router
|
||||||
|
@ -81,33 +75,9 @@ export default function Editor() {
|
||||||
setSecondaryTool(tool);
|
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);
|
const deferredSource = useDeferredValue(source);
|
||||||
|
|
||||||
useEffect(() => {
|
const checkResult: CheckResult = useMemo(() => {
|
||||||
if (deferredSource == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pythonSource, settingsSource } = deferredSource;
|
const { pythonSource, settingsSource } = deferredSource;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -161,116 +131,62 @@ export default function Editor() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setCheckResult({
|
return {
|
||||||
diagnostics,
|
diagnostics,
|
||||||
error: null,
|
error: null,
|
||||||
secondary,
|
secondary,
|
||||||
});
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setCheckResult({
|
return {
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
error: (e as Error).message,
|
error: (e as Error).message,
|
||||||
secondary: null,
|
secondary: null,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}, [deferredSource, secondaryTool]);
|
}, [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 (
|
return (
|
||||||
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
|
<>
|
||||||
<Header
|
<PanelGroup direction="horizontal" autoSaveId="main">
|
||||||
edit={source ? source.revision : null}
|
<PrimarySideBar onSelectTool={(tool) => setTab(tool)} selected={tab} />
|
||||||
theme={theme}
|
<Panel id="main" order={0} className="my-2" minSize={10}>
|
||||||
version={ruffVersion}
|
<SourceEditor
|
||||||
onChangeTheme={setTheme}
|
visible={tab === "Source"}
|
||||||
onShare={handleShare}
|
source={source.pythonSource}
|
||||||
/>
|
theme={theme}
|
||||||
|
diagnostics={checkResult.diagnostics}
|
||||||
<div className="flex flex-grow">
|
onChange={onSourceChanged}
|
||||||
{source ? (
|
/>
|
||||||
<PanelGroup direction="horizontal" autoSaveId="main">
|
<SettingsEditor
|
||||||
<PrimarySideBar
|
visible={tab === "Settings"}
|
||||||
onSelectTool={(tool) => setTab(tool)}
|
source={source.settingsSource}
|
||||||
selected={tab}
|
theme={theme}
|
||||||
/>
|
onChange={onSettingsChanged}
|
||||||
<Panel id="main" order={0} className="my-2" minSize={10}>
|
/>
|
||||||
<SourceEditor
|
</Panel>
|
||||||
visible={tab === "Source"}
|
{secondaryTool != null && (
|
||||||
source={source.pythonSource}
|
<>
|
||||||
|
<HorizontalResizeHandle />
|
||||||
|
<Panel
|
||||||
|
id="secondary-panel"
|
||||||
|
order={1}
|
||||||
|
className={"my-2"}
|
||||||
|
minSize={10}
|
||||||
|
>
|
||||||
|
<SecondaryPanel
|
||||||
theme={theme}
|
theme={theme}
|
||||||
diagnostics={checkResult.diagnostics}
|
tool={secondaryTool}
|
||||||
onChange={handlePythonSourceChange}
|
result={checkResult.secondary}
|
||||||
/>
|
|
||||||
<SettingsEditor
|
|
||||||
visible={tab === "Settings"}
|
|
||||||
source={source.settingsSource}
|
|
||||||
theme={theme}
|
|
||||||
onChange={handleSettingsSourceChange}
|
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
{secondaryTool != null && (
|
</>
|
||||||
<>
|
)}
|
||||||
<HorizontalResizeHandle />
|
<SecondarySideBar
|
||||||
<Panel
|
selected={secondaryTool}
|
||||||
id="secondary-panel"
|
onSelected={handleSecondaryToolSelected}
|
||||||
order={1}
|
/>
|
||||||
className={"my-2"}
|
</PanelGroup>
|
||||||
minSize={10}
|
|
||||||
>
|
|
||||||
<SecondaryPanel
|
|
||||||
theme={theme}
|
|
||||||
tool={secondaryTool}
|
|
||||||
result={checkResult.secondary}
|
|
||||||
/>
|
|
||||||
</Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<SecondarySideBar
|
|
||||||
selected={secondaryTool}
|
|
||||||
onSelected={handleSecondaryToolSelected}
|
|
||||||
/>
|
|
||||||
</PanelGroup>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{checkResult.error && tab === "Source" ? (
|
{checkResult.error && tab === "Source" ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -283,7 +199,7 @@ export default function Editor() {
|
||||||
<ErrorMessage>{checkResult.error}</ErrorMessage>
|
<ErrorMessage>{checkResult.error}</ErrorMessage>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</main>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import Editor, { BeforeMount, Monaco } from "@monaco-editor/react";
|
import Editor, { BeforeMount, Monaco } from "@monaco-editor/react";
|
||||||
import { MarkerSeverity, MarkerTag } from "monaco-editor";
|
import { MarkerSeverity, MarkerTag } from "monaco-editor";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { Diagnostic } from "../pkg";
|
import { Diagnostic } from "../pkg/ruff_wasm";
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
|
|
||||||
export default function SourceEditor({
|
export default function SourceEditor({
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* Light and dark mode theming.
|
* Light and dark mode theming.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export type Theme = "dark" | "light";
|
export type Theme = "dark" | "light";
|
||||||
|
|
||||||
export function useTheme(): [Theme, (theme: Theme) => void] {
|
export function useTheme(): [Theme, (theme: Theme) => void] {
|
||||||
const [localTheme, setLocalTheme] = useState<Theme>("light");
|
const [localTheme, setLocalTheme] = useState<Theme>(() =>
|
||||||
|
detectInitialTheme(),
|
||||||
|
);
|
||||||
|
|
||||||
const setTheme = (mode: Theme) => {
|
const setTheme = (mode: Theme) => {
|
||||||
if (mode === "dark") {
|
if (mode === "dark") {
|
||||||
|
@ -18,18 +20,18 @@ export function useTheme(): [Theme, (theme: Theme) => void] {
|
||||||
setLocalTheme(mode);
|
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];
|
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 React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import Editor from "./Editor";
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { loader } from "@monaco-editor/react";
|
import Chrome from "./Editor/Chrome";
|
||||||
import { setupMonaco } from "./Editor/setupMonaco";
|
|
||||||
|
|
||||||
loader.init().then(setupMonaco);
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Editor />
|
<Chrome />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue