Skip to content

Commit fc89e86

Browse files
Copilotna-trium-144
andcommitted
Implement RuntimeContext type and useRuntime hook, refactor exec.tsx and repl.tsx
Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com>
1 parent e9ed54a commit fc89e86

File tree

7 files changed

+204
-165
lines changed

7 files changed

+204
-165
lines changed

app/terminal/exec.tsx

Lines changed: 37 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
"use client";
22

3-
import { usePyodide } from "./python/pyodide";
43
import {
54
clearTerminal,
65
getRows,
76
hideCursor,
87
systemMessageColor,
98
useTerminal,
109
} from "./terminal";
11-
import { useWandbox } from "./wandbox/wandbox";
12-
import { ReplOutput, writeOutput } from "./repl";
10+
import { writeOutput } from "./repl";
1311
import { useState } from "react";
1412
import { useEmbedContext } from "./embedContext";
13+
import { useRuntime } from "./runtime";
14+
import { useWandbox } from "./wandbox/wandbox";
1515

1616
export type ExecLang = "python" | "cpp";
1717

@@ -36,63 +36,50 @@ export function ExecFile(props: ExecProps) {
3636
});
3737
const sectionContext = useEmbedContext();
3838

39-
const pyodide = usePyodide();
39+
const runtime = useRuntime(props.language);
4040
const wandbox = useWandbox();
4141

4242
// 表示するコマンドライン文字列
4343
let commandline: string;
44-
// trueの間 (初期化しています...) と表示される
45-
let runtimeInitializing: boolean;
46-
// 初期化処理が必要な場合の関数
47-
let beforeExec: (() => Promise<void>) | null = null;
44+
if (props.language === "python") {
45+
if (props.filenames.length !== 1) {
46+
throw new Error("Pythonの実行にはファイル名が1つ必要です");
47+
}
48+
commandline = `python ${props.filenames[0]}`;
49+
} else if (props.language === "cpp") {
50+
if (!props.filenames || props.filenames.length === 0) {
51+
throw new Error("C++の実行には filenames プロパティが必要です");
52+
}
53+
commandline = wandbox.getCommandlineStr("C++", props.filenames);
54+
} else {
55+
props.language satisfies never;
56+
commandline = `エラー: 非対応の言語 ${props.language}`;
57+
}
58+
59+
const runtimeInitializing = runtime.initializing;
60+
const beforeExec = runtime.ready ? null : runtime.init;
61+
const exec = () => runtime.runFiles(props.filenames);
62+
4863
// 実行中です... と表示される
4964
const [isExecuting, setIsExecuting] = useState<boolean>(false);
50-
// 実際に実行する関数
51-
let exec: (() => Promise<ReplOutput[]>) | null = null;
52-
switch (props.language) {
53-
case "python":
54-
if (props.filenames.length !== 1) {
55-
throw new Error("Pythonの実行にはファイル名が1つ必要です");
56-
}
57-
commandline = `python ${props.filenames[0]}`;
58-
runtimeInitializing = pyodide.initializing;
59-
beforeExec = pyodide.ready ? null : pyodide.init;
60-
exec = () => pyodide.runFile(props.filenames[0]);
61-
break;
62-
case "cpp":
63-
if (!props.filenames || props.filenames.length === 0) {
64-
throw new Error("C++の実行には filenames プロパティが必要です");
65-
}
66-
commandline = wandbox.getCommandlineStr("C++", props.filenames);
67-
runtimeInitializing = false;
68-
exec = () => wandbox.runFiles("C++", props.filenames);
69-
break;
70-
default:
71-
props.language satisfies never;
72-
commandline = `エラー: 非対応の言語 ${props.language}`;
73-
runtimeInitializing = false;
74-
break;
75-
}
7665

7766
const onClick = async () => {
78-
if (exec) {
79-
if (beforeExec) {
80-
clearTerminal(terminalInstanceRef.current!);
81-
terminalInstanceRef.current!.write(
82-
systemMessageColor("(初期化しています...しばらくお待ちください)")
83-
);
84-
await beforeExec();
85-
}
86-
clearTerminal(terminalInstanceRef.current!);
87-
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
88-
setIsExecuting(true);
89-
const outputs = await exec();
90-
setIsExecuting(false);
67+
if (beforeExec) {
9168
clearTerminal(terminalInstanceRef.current!);
92-
writeOutput(terminalInstanceRef.current!, outputs, false);
93-
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
94-
sectionContext?.setExecResult(props.filenames.join(","), outputs);
69+
terminalInstanceRef.current!.write(
70+
systemMessageColor("(初期化しています...しばらくお待ちください)")
71+
);
72+
await beforeExec();
9573
}
74+
clearTerminal(terminalInstanceRef.current!);
75+
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
76+
setIsExecuting(true);
77+
const outputs = await exec();
78+
setIsExecuting(false);
79+
clearTerminal(terminalInstanceRef.current!);
80+
writeOutput(terminalInstanceRef.current!, outputs, false);
81+
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
82+
sectionContext?.setExecResult(props.filenames.join(","), outputs);
9683
};
9784
return (
9885
<div className="relative">

app/terminal/python/embedded.tsx

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"use client";
22

3-
import { useMemo } from "react";
4-
import { ReplTerminal, ReplOutput, ReplCommand } from "../repl";
5-
import { usePyodide } from "./pyodide";
3+
import { ReplTerminal } from "../repl";
64

75
export function PythonEmbeddedTerminal({
86
terminalId,
@@ -11,55 +9,11 @@ export function PythonEmbeddedTerminal({
119
terminalId: string;
1210
content: string;
1311
}) {
14-
const initCommands = useMemo(() => splitContents(content), [content]);
15-
const {
16-
init,
17-
initializing,
18-
ready,
19-
runPython,
20-
checkSyntax,
21-
mutex,
22-
interrupt,
23-
} = usePyodide();
24-
2512
return (
2613
<ReplTerminal
2714
terminalId={terminalId}
28-
initRuntime={init}
29-
runtimeInitializing={initializing}
30-
runtimeReady={ready}
31-
initCommand={initCommands}
32-
mutex={mutex}
33-
prompt=">>> "
34-
promptMore="... "
3515
language="python"
36-
tabSize={4}
37-
sendCommand={runPython}
38-
checkSyntax={checkSyntax}
39-
interrupt={interrupt}
16+
content={content}
4017
/>
4118
);
4219
}
43-
44-
function splitContents(contents: string): ReplCommand[] {
45-
const initCommands: { command: string; output: ReplOutput[] }[] = [];
46-
for (const line of contents.split("\n")) {
47-
if (line.startsWith(">>> ")) {
48-
// Remove the prompt from the command
49-
initCommands.push({ command: line.slice(4), output: [] });
50-
} else if (line.startsWith("... ")) {
51-
if (initCommands.length > 0) {
52-
initCommands[initCommands.length - 1].command += "\n" + line.slice(4);
53-
}
54-
} else {
55-
// プロンプトを含まない行は前のコマンドの出力
56-
if (initCommands.length > 0) {
57-
initCommands[initCommands.length - 1].output.push({
58-
type: "stdout",
59-
message: line,
60-
});
61-
}
62-
}
63-
}
64-
return initCommands;
65-
}

app/terminal/python/page.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,14 @@
33
import { EditorComponent } from "../editor";
44
import { ExecFile } from "../exec";
55
import { ReplTerminal } from "../repl";
6-
import { usePyodide } from "./pyodide";
76

87
export default function PythonPage() {
9-
const { init, ready, initializing, runPython, checkSyntax, mutex, interrupt } =
10-
usePyodide();
118
return (
129
<div className="p-4 flex flex-col gap-4">
1310
<ReplTerminal
1411
terminalId=""
15-
initRuntime={init}
16-
runtimeInitializing={initializing}
17-
runtimeReady={ready}
18-
initMessage="Welcome to Pyodide Terminal!"
19-
prompt=">>> "
20-
promptMore="... "
2112
language="python"
22-
tabSize={4}
23-
mutex={mutex}
24-
sendCommand={runPython}
25-
checkSyntax={checkSyntax}
26-
interrupt={interrupt}
13+
initMessage="Welcome to Pyodide Terminal!"
2714
/>
2815
<EditorComponent
2916
language="python"

app/terminal/python/pyodide.tsx

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,14 @@ import {
99
useContext,
1010
useEffect,
1111
} from "react";
12-
import { SyntaxStatus, ReplOutput } from "../repl";
12+
import { SyntaxStatus, ReplOutput, ReplCommand } from "../repl";
1313
import { Mutex, MutexInterface } from "async-mutex";
1414
import { useEmbedContext } from "../embedContext";
15+
import { RuntimeContext } from "../runtime";
1516

16-
interface IPyodideContext {
17-
init: () => Promise<void>;
18-
initializing: boolean;
19-
ready: boolean;
20-
mutex: MutexInterface;
17+
interface IPyodideContext extends RuntimeContext {
2118
runPython: (code: string) => Promise<ReplOutput[]>;
2219
runFile: (name: string) => Promise<ReplOutput[]>;
23-
checkSyntax: (code: string) => Promise<SyntaxStatus>;
24-
interrupt: () => void;
2520
}
2621

2722
const PyodideContext = createContext<IPyodideContext>(null!);
@@ -191,6 +186,44 @@ export function PyodideProvider({ children }: { children: ReactNode }) {
191186
[ready]
192187
);
193188

189+
const runFiles = useCallback(
190+
async (filenames: string[]): Promise<ReplOutput[]> => {
191+
if (filenames.length !== 1) {
192+
return [
193+
{
194+
type: "error",
195+
message: "Python execution requires exactly one filename",
196+
},
197+
];
198+
}
199+
return runFile(filenames[0]);
200+
},
201+
[runFile]
202+
);
203+
204+
const splitContents = useCallback((content: string): ReplCommand[] => {
205+
const initCommands: { command: string; output: ReplOutput[] }[] = [];
206+
for (const line of content.split("\n")) {
207+
if (line.startsWith(">>> ")) {
208+
// Remove the prompt from the command
209+
initCommands.push({ command: line.slice(4), output: [] });
210+
} else if (line.startsWith("... ")) {
211+
if (initCommands.length > 0) {
212+
initCommands[initCommands.length - 1].command += "\n" + line.slice(4);
213+
}
214+
} else {
215+
// Lines without prompt are output from the previous command
216+
if (initCommands.length > 0) {
217+
initCommands[initCommands.length - 1].output.push({
218+
type: "stdout",
219+
message: line,
220+
});
221+
}
222+
}
223+
}
224+
return initCommands;
225+
}, []);
226+
194227
return (
195228
<PyodideContext.Provider
196229
value={{
@@ -201,7 +234,12 @@ export function PyodideProvider({ children }: { children: ReactNode }) {
201234
checkSyntax,
202235
mutex: mutex.current,
203236
runFile,
237+
runFiles,
204238
interrupt,
239+
splitContents,
240+
prompt: ">>> ",
241+
promptMore: "... ",
242+
tabSize: 4,
205243
}}
206244
>
207245
{children}

0 commit comments

Comments
 (0)