Skip to content

Commit

Permalink
Add LiveCodes as playground and test runner (#269)
Browse files Browse the repository at this point in the history
* add LiveCodes as playground and test runner

* add Ruby to LiveCodes playground

* add Lua to LiveCodes playground and run tests

* disable LiveCodes autoupdate

* add Jupyter to LiveCodes playground

* fix setting code in LiveCodes

* set theme of LiveCodes

* use stable LiveCodes release

* add a comment for the source of lua test runner

* use LiveCodes for PHP, Go & C

* remove pyodide (used from within LiveCodes)

* upgrade LiveCodes
  • Loading branch information
hatemhosny committed Nov 1, 2023
1 parent 059a8dd commit ee250fc
Show file tree
Hide file tree
Showing 18 changed files with 512 additions and 3,244 deletions.
233 changes: 233 additions & 0 deletions components/playgroundEditor/LiveCodes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import type { Config, Playground } from "livecodes";
import LiveCodesPlayground from "livecodes/react";
import { luaTestRunner, type Language } from "lib/playground/livecodes";
import { useDarkTheme } from "hooks/darkTheme";

export default function LiveCodes({
language,
code,
setCode,
tests,
}: {
language: Language;
code: string;
setCode: Dispatch<SetStateAction<string>>;
tests: string;
}) {
const [playground, setPlayground] = useState<Playground | undefined>();
const [darkTheme] = useDarkTheme();

const onReady = (sdk: Playground) => {
setPlayground(sdk);
sdk.watch("ready", async () => {
await sdk.run();
if (language === "javascript" || language === "typescript") {
await sdk.runTests();
}
});
sdk.watch("code", (changed) => {
setCode(changed.code.script.content);
});
};

useEffect(() => {
playground?.setConfig({ theme: darkTheme ? "dark" : "light" });
}, [playground, darkTheme]);

const baseConfig: Partial<Config> = {
autoupdate: false,
languages: [language === "jupyter" ? "python-wasm" : language],
script: {
language: language === "jupyter" ? "python-wasm" : language,
content: code,
},
tools: {
enabled: ["console"],
active: "console",
status: "full",
},
};

const getJSTSConfig = (
lang: "javascript" | "typescript",
jsCode: string,
test: string
): Partial<Config> => {
const editTest = (src: string) =>
src.replace(
/import\s+((?:.|\s)*?)\s+from\s+('|").*?('|")/g,
"import $1 from './script'"
);
return {
...baseConfig,
script: {
language: lang,
content: jsCode,
},
tests: {
language: lang,
content: editTest(test),
},
tools: {
enabled: [
"console",
"tests",
...(lang === "typescript" ? ["compiled"] : []),
] as Config["tools"]["enabled"],
active: "tests",
status: "full",
},
autotest: true,
};
};

const getPythonConfig = (pyCode: string): Partial<Config> => {
const addTestRunner = (src: string) => {
const sep = 'if __name__ == "__main__":\n';
const [algCode, run] = src.split(sep);
const comment =
run
?.split("\n")
.map((line) => `# ${line}`)
.join("\n") || "";
const testRunner = `\n import doctest\n doctest.testmod(verbose=True)`;
return `${algCode}${sep}${comment}${testRunner}`;
};
return {
...baseConfig,
languages: ["python-wasm"],
script: {
language: "python-wasm",
content: addTestRunner(pyCode),
},
};
};

const getJupyterConfig = (jsonCode: string): Partial<Config> => {
const getPyCode = (src: string) => {
try {
const nb: {
cells: Array<{ ["cell_type"]: string; source: string[] }>;
} = JSON.parse(src);
return nb.cells
.filter((c) => c.cell_type === "code")
.map((c) => c.source.join(""))
.join("\n\n");
} catch {
return "";
}
};
return {
...baseConfig,
languages: ["python-wasm"],
script: {
language: "python-wasm",
content: getPyCode(jsonCode),
},
tools: {
enabled: ["console"],
active: "console",
status: "open",
},
};
};

const getRConfig = (rCode: string): Partial<Config> => {
const editCode = (src: string) =>
src.replace(/# Example:\n# /g, "# Example:\n");
return {
...baseConfig,
script: {
language: "r",
content: editCode(rCode),
},
tools: {
enabled: ["console"],
active: "console",
status: "open",
},
};
};

const getRubyConfig = (rubyCode: string): Partial<Config> => ({
...baseConfig,
script: {
language: "ruby",
content: rubyCode,
},
});

const getLuaConfig = (luaCode: string, test: string): Partial<Config> => {
const pattern = /\n\s*local\s+(\S+)\s+=\s+require.*\n/g;
const matches = test.matchAll(pattern);
const fnName = [...matches][0]?.[1] || "return";
const content = `
${luaCode.replace("return", `local ${fnName} =`)}
${test.replace(pattern, "\n")}`.trimStart();

return {
...baseConfig,
languages: ["lua-wasm"],
script: {
language: "lua-wasm",
content,
hiddenContent: luaTestRunner,
},
};
};

const getPhpConfig = (phpCode: string): Partial<Config> => ({
...baseConfig,
languages: ["php-wasm"],
script: {
language: "php-wasm",
content: phpCode,
},
tools: {
enabled: ["console"],
active: "console",
status: "open",
},
});

const getCConfig = (cCode: string): Partial<Config> => ({
...baseConfig,
languages: ["cpp-wasm"],
script: {
language: "cpp-wasm",
content: cCode,
},
});

const config: Partial<Config> =
language === "javascript" || language === "typescript"
? getJSTSConfig(language, code, tests)
: language === "python"
? getPythonConfig(code)
: language === "jupyter"
? getJupyterConfig(code)
: language === "r"
? getRConfig(code)
: language === "ruby"
? getRubyConfig(code)
: language === "lua"
? getLuaConfig(code, tests)
: language === "php"
? getPhpConfig(code)
: language === "c"
? getCConfig(code)
: baseConfig;

return (
<LiveCodesPlayground
appUrl="https://v17.livecodes.io/"
loading="eager"
config={config}
style={{ borderRadius: "0", resize: "none" }}
sdkReady={onReady}
/>
);
}
136 changes: 136 additions & 0 deletions components/playgroundEditor/PlaygroundEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* eslint-disable no-alert */
import { Button, LinearProgress } from "@material-ui/core";
import Editor from "@monaco-editor/react";
import React, {
useEffect,
Dispatch,
SetStateAction,
useState,
useRef,
useMemo,
} from "react";
import useTranslation from "hooks/translation";
import PlayArrow from "@material-ui/icons/PlayArrow";
import { useDarkTheme } from "hooks/darkTheme";
import { XTerm } from "xterm-for-react";
import { FitAddon } from "xterm-addon-fit";
import CodeRunner from "lib/playground/codeRunner";
import PistonCodeRunner from "lib/playground/pistonCodeRunner";
import classes from "./PlaygroundEditor.module.css";

function getMonacoLanguageName(language: string) {
switch (language) {
case "c-plus-plus":
return "cpp";
case "c-sharp":
return "cs";
default:
return language;
}
}

export default function PlaygroundEditor({
language,
code,
setCode,
}: {
language: string;
code: string;
setCode: Dispatch<SetStateAction<string>>;
}) {
const t = useTranslation();
const [darkTheme] = useDarkTheme();
const xtermRef = useRef<XTerm>();
const [ready, setReady] = useState(false);
const [disabled, setDisabled] = useState(false);

const codeRunner = useMemo<CodeRunner>(() => {
const runner = new PistonCodeRunner(xtermRef, t);
setTimeout(() => {
runner.load(code, language).then((r) => {
setReady(true);
setDisabled(!r);
});
});
return runner;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fitAddon = useMemo(() => new FitAddon(), []);

useEffect(() => {
function resizeHandler() {
fitAddon.fit();
}
resizeHandler();
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("resize", resizeHandler);
};
}, [fitAddon]);

useEffect(() => {
(async () => {
xtermRef.current.terminal.writeln(`${t("playgroundWelcome")}\n`);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className={classes.root}>
<LinearProgress
style={{
opacity: ready ? 0 : 1,
position: "absolute",
width: "100%",
zIndex: 10000,
}}
/>
<div className={classes.editor}>
<Editor
language={getMonacoLanguageName(language)}
value={code}
onChange={setCode}
options={{
automaticLayout: true,
padding: {
top: 15,
bottom: 15,
},
}}
theme={darkTheme ? "vs-dark" : "vs-light"}
/>
<Button
disabled={!ready || disabled}
onClick={() => {
setDisabled(true);
codeRunner.run(code).finally(() => {
setDisabled(false);
});
}}
className={classes.runBtn}
variant="contained"
color="primary"
startIcon={<PlayArrow />}
>
{t("playgroundRunCode")}
</Button>
</div>
<div className={classes.output}>
<XTerm
className={classes.xterm}
ref={xtermRef}
options={{ convertEol: true }}
addons={[fitAddon]}
onKey={(event) => {
if (event.domEvent.ctrlKey && event.domEvent.code === "KeyC") {
event.domEvent.preventDefault();
navigator.clipboard.writeText(
xtermRef.current.terminal.getSelection()
);
}
}}
/>
</div>
</div>
);
}
Loading

0 comments on commit ee250fc

Please sign in to comment.