Skip to content

Commit f2582fd

Browse files
committed
Red Knot Playground
1 parent 597c5f9 commit f2582fd

35 files changed

+748
-29
lines changed

crates/red_knot_wasm/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,20 @@ impl Workspace {
5656

5757
#[wasm_bindgen(js_name = "openFile")]
5858
pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> {
59+
let path = SystemPath::new(path);
5960
self.system
6061
.fs
6162
.write_file(path, contents)
6263
.map_err(into_error)?;
6364

65+
File::sync_path(&mut self.db, path);
6466
let file = system_path_to_file(&self.db, path).expect("File to exist");
65-
file.sync(&mut self.db);
6667

6768
self.db.workspace().open_file(&mut self.db, file);
6869

6970
Ok(FileHandle {
7071
file,
71-
path: SystemPath::new(path).to_path_buf(),
72+
path: path.to_path_buf(),
7273
})
7374
}
7475

@@ -261,7 +262,7 @@ impl System for WasmSystem {
261262
&'a self,
262263
path: &SystemPath,
263264
) -> ruff_db::system::Result<
264-
Box<dyn Iterator<Item = ruff_db::system::Result<DirectoryEntry>> + 'a>,
265+
Box<dyn Iterator<Item=ruff_db::system::Result<DirectoryEntry>> + 'a>,
265266
> {
266267
Ok(Box::new(self.fs.read_directory(path)?))
267268
}

playground/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
</head>
3535
<body>
3636
<div id="root"></div>
37-
<script type="module" src="/src/main.tsx"></script>
37+
<script type="module" src="/src/ruff.tsx"></script>
3838
</body>
3939
</html>

playground/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"build:wasm": "wasm-pack build ../crates/ruff_wasm --target web --out-dir ../../playground/src/pkg",
7+
"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",
88
"build": "tsc && vite build",
99
"check": "npm run lint && npm run tsc",
1010
"dev": "vite",
11-
"dev:wasm": "wasm-pack build ../crates/ruff_wasm --dev --target web --out-dir ../../playground/src/pkg",
11+
"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",
1212
"fmt": "prettier --cache -w .",
1313
"lint": "eslint --cache --ext .ts,.tsx src",
1414
"preview": "vite preview",

playground/red_knot/index.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="referrer" content="no-referrer-when-downgrade" />
7+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
8+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
9+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
10+
<meta name="msapplication-TileColor" content="#d7ff64" />
11+
<meta name="theme-color" content="#ffffff" />
12+
<title>Playground | Red Knot</title>
13+
<meta
14+
name="description"
15+
content="An in-browser playground for Red Knot, an extremely fast Python type checker written in Rust."
16+
/>
17+
<meta name="keywords" content="ruff, python, rust, webassembly, wasm" />
18+
<meta name="twitter:card" content="summary_large_image" />
19+
<meta name="twitter:site" content="@astral_sh" />
20+
<meta property="og:title" content="Playground | Ruff" />
21+
<meta
22+
property="og:description"
23+
content="An in-browser playground for Red Knot, an extremely fast Python type checker written in Rust."
24+
/>
25+
<meta property="og:url" content="https://play.ruff.rs" />
26+
<meta property="og:image" content="/Ruff.png" />
27+
<link rel="canonical" href="https://play.ruff.rs" />
28+
<link rel="icon" href="/favicon.ico" />
29+
<script
30+
src="https://cdn.usefathom.com/script.js"
31+
data-site="XWUDIXNB"
32+
defer
33+
></script>
34+
</head>
35+
<body>
36+
<div id="root"></div>
37+
<script type="module" src="../src/red_knot.tsx"></script>
38+
</body>
39+
</html>

playground/src/red_knot.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
import ReactDOM from "react-dom/client";
3+
import "./index.css";
4+
5+
import Chrome from "./red_knot/Chrome";
6+
7+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
8+
<React.StrictMode>
9+
<Chrome />
10+
</React.StrictMode>,
11+
);

playground/src/red_knot/Chrome.tsx

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { useCallback, useRef, useState } from "react";
2+
import Header from "../shared/Header";
3+
import { useTheme } from "../shared/theme";
4+
import { default as Editor } from "./Editor";
5+
import initRedKnot, {
6+
Workspace,
7+
Settings,
8+
TargetVersion,
9+
FileHandle,
10+
} from "./red_knot_wasm";
11+
import { loader } from "@monaco-editor/react";
12+
import { setupMonaco } from "../shared/setupMonaco";
13+
import { Panel, PanelGroup } from "react-resizable-panels";
14+
import { Files } from "./Files";
15+
16+
type CurrentFile = {
17+
handle: FileHandle;
18+
content: string;
19+
};
20+
21+
export type FileIndex = {
22+
[name: string]: FileHandle;
23+
};
24+
25+
export default function Chrome() {
26+
const initPromise = useRef<null | Promise<void>>(null);
27+
const [workspace, setWorkspace] = useState<null | Workspace>(null);
28+
29+
const [files, setFiles] = useState<FileIndex>({});
30+
31+
// The revision gets incremented everytime any persisted state changes.
32+
const [revision, setRevision] = useState(0);
33+
const [version, setVersion] = useState("");
34+
35+
const [currentFile, setCurrentFile] = useState<CurrentFile | null>(null);
36+
37+
const [theme, setTheme] = useTheme();
38+
39+
const handleShare = useCallback(() => {
40+
alert("TODO");
41+
}, []);
42+
43+
if (initPromise.current == null) {
44+
initPromise.current = startPlayground()
45+
.then(({ version }) => {
46+
const settings = new Settings(TargetVersion.Py312);
47+
const workspace = new Workspace("/", settings);
48+
setVersion(version);
49+
setWorkspace(workspace);
50+
51+
const content = "import os";
52+
const main = workspace.openFile("main.py", content);
53+
54+
setFiles({
55+
"main.py": main,
56+
});
57+
58+
setCurrentFile({
59+
handle: main,
60+
content,
61+
});
62+
63+
setRevision(1);
64+
})
65+
.catch((error) => {
66+
console.error("Failed to initialize playground.", error);
67+
});
68+
}
69+
70+
const handleSourceChanged = useCallback(
71+
(source: string) => {
72+
if (
73+
workspace == null ||
74+
currentFile == null ||
75+
source == currentFile.content
76+
) {
77+
return;
78+
}
79+
80+
workspace.updateFile(currentFile.handle, source);
81+
82+
setCurrentFile({
83+
...currentFile,
84+
content: source,
85+
});
86+
setRevision((revision) => revision + 1);
87+
},
88+
[workspace, currentFile],
89+
);
90+
91+
const handleFileClicked = useCallback(
92+
(file: FileHandle) => {
93+
if (workspace == null) {
94+
return;
95+
}
96+
97+
setCurrentFile({
98+
handle: file,
99+
content: workspace.sourceText(file),
100+
});
101+
},
102+
[workspace],
103+
);
104+
105+
const handleFileAdded = useCallback(
106+
(name: string) => {
107+
if (workspace == null) {
108+
return;
109+
}
110+
111+
const handle = workspace.openFile(name, "");
112+
setCurrentFile({
113+
handle,
114+
content: "",
115+
});
116+
117+
setFiles((files) => ({ ...files, [name]: handle }));
118+
setRevision((revision) => revision + 1);
119+
},
120+
[workspace],
121+
);
122+
123+
const handleFileRemoved = useCallback(
124+
(file: FileHandle) => {
125+
if (workspace == null) {
126+
return;
127+
}
128+
129+
const fileEntries = Object.entries(files);
130+
const index = fileEntries.findIndex(([, value]) => value === file);
131+
132+
if (index === -1) {
133+
return;
134+
}
135+
136+
// Remove the file
137+
fileEntries.splice(index, 1);
138+
139+
if (currentFile?.handle === file) {
140+
const newCurrentFile =
141+
index > 0 ? fileEntries[index - 1] : fileEntries[index];
142+
143+
if (newCurrentFile == null) {
144+
setCurrentFile(null);
145+
} else {
146+
const handle = newCurrentFile[1];
147+
setCurrentFile({
148+
handle,
149+
content: workspace.sourceText(handle),
150+
});
151+
}
152+
}
153+
154+
workspace.closeFile(file);
155+
setFiles(Object.fromEntries(fileEntries));
156+
setRevision((revision) => revision + 1);
157+
},
158+
[currentFile, workspace, files],
159+
);
160+
161+
const handleFileRenamed = useCallback(
162+
(file: FileHandle, newName: string) => {
163+
if (workspace == null) {
164+
return;
165+
}
166+
167+
const content = workspace.sourceText(file);
168+
workspace.closeFile(file);
169+
const newFile = workspace.openFile(newName, content);
170+
171+
if (currentFile?.handle === file) {
172+
setCurrentFile({
173+
content,
174+
handle: newFile,
175+
});
176+
}
177+
178+
setFiles((files) => {
179+
const entries = Object.entries(files);
180+
const index = entries.findIndex(([, value]) => value === file);
181+
182+
entries.splice(index, 1, [newName, newFile]);
183+
184+
return Object.fromEntries(entries);
185+
});
186+
187+
setRevision((revision) => (revision += 1));
188+
},
189+
[workspace, currentFile],
190+
);
191+
192+
return (
193+
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
194+
<Header
195+
edit={revision}
196+
theme={theme}
197+
version={version}
198+
onChangeTheme={setTheme}
199+
onShare={handleShare}
200+
/>
201+
202+
<div className="flex grow">
203+
<PanelGroup direction="horizontal" autoSaveId="main">
204+
{workspace != null && currentFile != null ? (
205+
<Panel
206+
id="main"
207+
order={0}
208+
className="flex flex-col gap-2"
209+
minSize={10}
210+
>
211+
<Files
212+
files={files}
213+
theme={theme}
214+
selected={currentFile?.handle ?? null}
215+
onAdd={handleFileAdded}
216+
onRename={handleFileRenamed}
217+
onSelected={handleFileClicked}
218+
onRemove={handleFileRemoved}
219+
/>
220+
221+
<div className="flex-grow">
222+
<Editor
223+
theme={theme}
224+
content={currentFile.content}
225+
onSourceChanged={handleSourceChanged}
226+
file={currentFile.handle}
227+
workspace={workspace}
228+
/>
229+
</div>
230+
</Panel>
231+
) : null}
232+
</PanelGroup>
233+
</div>
234+
</main>
235+
);
236+
}
237+
238+
// Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state.
239+
async function startPlayground(): Promise<{
240+
version: string;
241+
}> {
242+
await initRedKnot();
243+
const monaco = await loader.init();
244+
245+
setupMonaco(monaco);
246+
247+
return {
248+
version: "0.0.0",
249+
};
250+
}

0 commit comments

Comments
 (0)