Skip to content

Commit

Permalink
Red Knot Playground
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Aug 11, 2024
1 parent 597c5f9 commit 1fa1770
Show file tree
Hide file tree
Showing 34 changed files with 744 additions and 26 deletions.
2 changes: 1 addition & 1 deletion playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/ruff.tsx"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"build:wasm": "wasm-pack build ../crates/ruff_wasm --target web --out-dir ../../playground/src/pkg",
"build:wasm": "wasm-pack build ../crates/ruff_wasm --target web --out-dir ../../playground/src/ruff/ruff_wasm && wasm-pack build ../crates/red_knot_wasm --target web --out-dir ../../playground/src/red_knot/red_knot_wasm",
"build": "tsc && vite build",
"check": "npm run lint && npm run tsc",
"dev": "vite",
"dev:wasm": "wasm-pack build ../crates/ruff_wasm --dev --target web --out-dir ../../playground/src/pkg",
"dev:wasm": "wasm-pack build ../crates/ruff_wasm --dev --target web --out-dir ../../playground/src/ruff/ruff_wasm && wasm-pack build ../crates/red_knot_wasm --dev --target web --out-dir ../../playground/src/red_knot/red_knot_wasm",
"fmt": "prettier --cache -w .",
"lint": "eslint --cache --ext .ts,.tsx src",
"preview": "vite preview",
Expand Down
39 changes: 39 additions & 0 deletions playground/red_knot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer-when-downgrade" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<meta name="msapplication-TileColor" content="#d7ff64" />
<meta name="theme-color" content="#ffffff" />
<title>Playground | Red Knot</title>
<meta
name="description"
content="An in-browser playground for Red Knot, an extremely fast Python type checker written in Rust."
/>
<meta name="keywords" content="ruff, python, rust, webassembly, wasm" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@astral_sh" />
<meta property="og:title" content="Playground | Ruff" />
<meta
property="og:description"
content="An in-browser playground for Red Knot, an extremely fast Python type checker written in Rust."
/>
<meta property="og:url" content="https://play.ruff.rs" />
<meta property="og:image" content="/Ruff.png" />
<link rel="canonical" href="https://play.ruff.rs" />
<link rel="icon" href="/favicon.ico" />
<script
src="https://cdn.usefathom.com/script.js"
data-site="XWUDIXNB"
defer
></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/red_knot.tsx"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions playground/src/red_knot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";

import Chrome from "./red_knot/Chrome";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Chrome />
</React.StrictMode>,
);
250 changes: 250 additions & 0 deletions playground/src/red_knot/Chrome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { useCallback, useRef, useState } from "react";
import Header from "../shared/Header";
import { useTheme } from "../shared/theme";
import { default as Editor } from "./Editor";
import initRedKnot, {
Workspace,
Settings,
TargetVersion,
FileHandle,
} from "./red_knot_wasm";
import { loader } from "@monaco-editor/react";
import { setupMonaco } from "../shared/setupMonaco";
import { Panel, PanelGroup } from "react-resizable-panels";
import { Files } from "./Files";

type CurrentFile = {
handle: FileHandle;
content: string;
};

export type FileIndex = {
[name: string]: FileHandle;
};

export default function Chrome() {
const initPromise = useRef<null | Promise<void>>(null);
const [workspace, setWorkspace] = useState<null | Workspace>(null);

const [files, setFiles] = useState<FileIndex>({});

// The revision gets incremented everytime any persisted state changes.
const [revision, setRevision] = useState(0);
const [version, setVersion] = useState("");

const [currentFile, setCurrentFile] = useState<CurrentFile | null>(null);

const [theme, setTheme] = useTheme();

const handleShare = useCallback(() => {
alert("TODO");
}, []);

if (initPromise.current == null) {
initPromise.current = startPlayground()
.then(({ version }) => {
const settings = new Settings(TargetVersion.Py312);
const workspace = new Workspace("/", settings);
setVersion(version);
setWorkspace(workspace);

const content = "import os";
const main = workspace.openFile("main.py", content);

setFiles({
"main.py": main,
});

setCurrentFile({
handle: main,
content,
});

setRevision(1);
})
.catch((error) => {
console.error("Failed to initialize playground.", error);
});
}

const handleSourceChanged = useCallback(
(source: string) => {
if (
workspace == null ||
currentFile == null ||
source == currentFile.content
) {
return;
}

workspace.updateFile(currentFile.handle, source);

setCurrentFile({
...currentFile,
content: source,
});
setRevision((revision) => revision + 1);
},
[workspace, currentFile],
);

const handleFileClicked = useCallback(
(file: FileHandle) => {
if (workspace == null) {
return;
}

setCurrentFile({
handle: file,
content: workspace.sourceText(file),
});
},
[workspace],
);

const handleFileAdded = useCallback(
(name: string) => {
if (workspace == null) {
return;
}

const handle = workspace.openFile(name, "");
setCurrentFile({
handle,
content: "",
});

setFiles((files) => ({ ...files, [name]: handle }));
setRevision((revision) => revision + 1);
},
[workspace],
);

const handleFileRemoved = useCallback(
(file: FileHandle) => {
if (workspace == null) {
return;
}

const fileEntries = Object.entries(files);
const index = fileEntries.findIndex(([, value]) => value === file);

if (index === -1) {
return;
}

// Remove the file
fileEntries.splice(index, 1);

if (currentFile?.handle === file) {
const newCurrentFile =
index > 0 ? fileEntries[index - 1] : fileEntries[index];

if (newCurrentFile == null) {
setCurrentFile(null);
} else {
const handle = newCurrentFile[1];
setCurrentFile({
handle,
content: workspace.sourceText(handle),
});
}
}

workspace.closeFile(file);
setFiles(Object.fromEntries(fileEntries));
setRevision((revision) => revision + 1);
},
[currentFile, workspace, files],
);

const handleFileRenamed = useCallback(
(file: FileHandle, newName: string) => {
if (workspace == null) {
return;
}

const content = workspace.sourceText(file);
workspace.closeFile(file);
const newFile = workspace.openFile(newName, content);

if (currentFile?.handle === file) {
setCurrentFile({
content,
handle: newFile,
});
}

setFiles((files) => {
const entries = Object.entries(files);
const index = entries.findIndex(([, value]) => value === file);

entries.splice(index, 1, [newName, newFile]);

return Object.fromEntries(entries);
});

setRevision((revision) => (revision += 1));
},
[workspace, currentFile],
);

return (
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
<Header
edit={revision}
theme={theme}
version={version}
onChangeTheme={setTheme}
onShare={handleShare}
/>

<div className="flex grow">
<PanelGroup direction="horizontal" autoSaveId="main">
{workspace != null && currentFile != null ? (
<Panel
id="main"
order={0}
className="flex flex-col gap-2"
minSize={10}
>
<Files
files={files}
theme={theme}
selected={currentFile?.handle ?? null}
onAdd={handleFileAdded}
onRename={handleFileRenamed}
onSelected={handleFileClicked}
onRemove={handleFileRemoved}
/>

<div className="flex-grow">
<Editor
theme={theme}
content={currentFile.content}
onSourceChanged={handleSourceChanged}
file={currentFile.handle}
workspace={workspace}
/>
</div>
</Panel>
) : null}
</PanelGroup>
</div>
</main>
);
}

// Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state.
async function startPlayground(): Promise<{
version: string;
}> {
await initRedKnot();
const monaco = await loader.init();

setupMonaco(monaco);

return {
version: "0.0.0",
};
}
Loading

0 comments on commit 1fa1770

Please sign in to comment.