-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
597c5f9
commit 1fa1770
Showing
34 changed files
with
744 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}; | ||
} |
Oops, something went wrong.