Skip to content

Commit 6cd6adb

Browse files
committed
異常終了時の表示
1 parent 1d714bc commit 6cd6adb

File tree

4 files changed

+136
-94
lines changed

4 files changed

+136
-94
lines changed

app/terminal/exec.tsx

Lines changed: 49 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
import chalk from "chalk";
44
import { usePyodide } from "./python/pyodide";
5-
import { clearTerminal, getRows, hideCursor, useTerminal } from "./terminal";
5+
import {
6+
clearTerminal,
7+
getRows,
8+
hideCursor,
9+
systemMessageColor,
10+
useTerminal,
11+
} from "./terminal";
612
import { useSectionCode } from "../[docs_id]/section";
713
import { useWandbox } from "./wandbox/wandbox";
14+
import { ReplOutput, writeOutput } from "./repl";
15+
import { useState } from "react";
816

917
export type ExecLang = "python" | "cpp";
1018

@@ -32,44 +40,25 @@ export function ExecFile(props: ExecProps) {
3240
const pyodide = usePyodide();
3341
const wandbox = useWandbox();
3442

43+
// 表示するコマンドライン文字列
3544
let commandline: string;
36-
let exec: () => Promise<void> | void;
45+
// trueの間 (初期化しています...) と表示される
3746
let runtimeInitializing: boolean;
47+
// 初期化処理が必要な場合の関数
48+
let beforeExec: (() => Promise<void>) | null = null;
49+
// 実行中です... と表示される
50+
const [isExecuting, setIsExecuting] = useState<boolean>(false);
51+
// 実際に実行する関数
52+
let exec: (() => Promise<ReplOutput[]>) | null = null;
3853
switch (props.language) {
3954
case "python":
4055
if (props.filenames.length !== 1) {
4156
throw new Error("Pythonの実行にはファイル名が1つ必要です");
4257
}
4358
commandline = `python ${props.filenames[0]}`;
4459
runtimeInitializing = pyodide.initializing;
45-
exec = async () => {
46-
if (!pyodide.ready) {
47-
clearTerminal(terminalInstanceRef.current!);
48-
terminalInstanceRef.current!.write(
49-
chalk.dim.bold.italic("(初期化しています...しばらくお待ちください)")
50-
);
51-
await pyodide.init();
52-
}
53-
clearTerminal(terminalInstanceRef.current!);
54-
const outputs = await pyodide.runFile(props.filenames[0]);
55-
for (let i = 0; i < outputs.length; i++) {
56-
const output = outputs[i];
57-
if(i > 0) {
58-
terminalInstanceRef.current!.writeln("");
59-
}
60-
// 出力内容に応じて色を変える
61-
const message = String(output.message).replace(/\n/g, "\r\n");
62-
switch (output.type) {
63-
case "error":
64-
terminalInstanceRef.current!.writeln(chalk.red(message));
65-
break;
66-
default:
67-
terminalInstanceRef.current!.writeln(message);
68-
break;
69-
}
70-
}
71-
sectionContext?.setExecResult(props.filenames[0], outputs);
72-
};
60+
beforeExec = pyodide.ready ? null : pyodide.init;
61+
exec = () => pyodide.runFile(props.filenames[0]);
7362
break;
7463
case "cpp":
7564
if (!props.filenames || props.filenames.length === 0) {
@@ -79,52 +68,47 @@ export function ExecFile(props: ExecProps) {
7968
? `${wandbox.cppOptions.commandline} ${props.filenames.join(" ")} && ./a.out`
8069
: "";
8170
runtimeInitializing = false;
82-
exec = async () => {
83-
clearTerminal(terminalInstanceRef.current!);
84-
const namesSource = props.filenames!.filter((name) =>
85-
name.endsWith(".cpp")
86-
);
87-
const namesAdditional = props.filenames!.filter(
88-
(name) => !name.endsWith(".cpp")
89-
);
90-
const outputs = await wandbox.runFiles(
91-
"C++",
92-
namesSource,
93-
namesAdditional
94-
);
95-
for (let i = 0; i < outputs.length; i++) {
96-
const output = outputs[i];
97-
if(i > 0) {
98-
terminalInstanceRef.current!.writeln("");
99-
}
100-
// 出力内容に応じて色を変える
101-
const message = String(output.message).replace(/\n/g, "\r\n");
102-
switch (output.type) {
103-
case "error":
104-
terminalInstanceRef.current!.write(chalk.red(message));
105-
break;
106-
default:
107-
terminalInstanceRef.current!.write(message);
108-
break;
109-
}
110-
}
111-
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
112-
sectionContext?.setExecResult(props.filenames.join(","), outputs);
113-
};
71+
const namesSource = props.filenames!.filter((name) =>
72+
name.endsWith(".cpp")
73+
);
74+
const namesAdditional = props.filenames!.filter(
75+
(name) => !name.endsWith(".cpp")
76+
);
77+
exec = () => wandbox.runFiles("C++", namesSource, namesAdditional);
11478
break;
11579
default:
11680
props.language satisfies never;
11781
commandline = `エラー: 非対応の言語 ${props.language}`;
11882
runtimeInitializing = false;
119-
exec = () => undefined;
12083
break;
12184
}
85+
86+
const onClick = async () => {
87+
if (exec) {
88+
if (beforeExec) {
89+
clearTerminal(terminalInstanceRef.current!);
90+
terminalInstanceRef.current!.write(
91+
systemMessageColor("(初期化しています...しばらくお待ちください)")
92+
);
93+
await beforeExec();
94+
}
95+
clearTerminal(terminalInstanceRef.current!);
96+
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
97+
setIsExecuting(true);
98+
const outputs = await exec();
99+
setIsExecuting(false);
100+
clearTerminal(terminalInstanceRef.current!);
101+
writeOutput(terminalInstanceRef.current!, outputs, false);
102+
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
103+
sectionContext?.setExecResult(props.filenames.join(","), outputs);
104+
}
105+
};
122106
return (
123107
<div className="relative">
124108
<div>
125109
<button
126110
className="btn btn-soft btn-primary rounded-tl-lg rounded-none"
127-
onClick={exec}
111+
onClick={onClick}
128112
disabled={!termReady || runtimeInitializing}
129113
>
130114
▶ 実行
@@ -134,7 +118,7 @@ export function ExecFile(props: ExecProps) {
134118
<div className="bg-base-300 p-4 pt-2 rounded-b-lg">
135119
<div ref={terminalRef} />
136120
</div>
137-
{runtimeInitializing && (
121+
{(runtimeInitializing || isExecuting) && (
138122
<div className="absolute z-10 inset-0 cursor-wait" />
139123
)}
140124
</div>

app/terminal/repl.tsx

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import { useCallback, useEffect, useRef } from "react";
44
import { highlightCodeToAnsi } from "./highlight";
55
import chalk from "chalk";
66
import { MutexInterface } from "async-mutex";
7-
import { clearTerminal, getRows, hideCursor, showCursor, useTerminal } from "./terminal";
7+
import {
8+
clearTerminal,
9+
getRows,
10+
hideCursor,
11+
showCursor,
12+
systemMessageColor,
13+
useTerminal,
14+
} from "./terminal";
815
import { useSectionCode } from "../[docs_id]/section";
16+
import { Terminal } from "@xterm/xterm";
917

1018
export interface ReplOutput {
11-
type: "stdout" | "stderr" | "error" | "return"; // 出力の種類
19+
type: "stdout" | "stderr" | "error" | "return" | "system"; // 出力の種類
1220
message: string; // 出力メッセージ
1321
}
1422
export interface ReplCommand {
@@ -17,6 +25,35 @@ export interface ReplCommand {
1725
}
1826
export type SyntaxStatus = "complete" | "incomplete" | "invalid"; // 構文チェックの結果
1927

28+
export function writeOutput(
29+
term: Terminal,
30+
outputs: ReplOutput[],
31+
endNewLine: boolean
32+
) {
33+
for (let i = 0; i < outputs.length; i++) {
34+
const output = outputs[i];
35+
if (i > 0) {
36+
term.writeln("");
37+
}
38+
// 出力内容に応じて色を変える
39+
const message = String(output.message).replace(/\n/g, "\r\n");
40+
switch (output.type) {
41+
case "error":
42+
term.write(chalk.red(message));
43+
break;
44+
case "system":
45+
term.write(systemMessageColor(message));
46+
break;
47+
default:
48+
term.write(message);
49+
break;
50+
}
51+
}
52+
if (endNewLine && outputs.length > 0) {
53+
term.writeln("");
54+
}
55+
}
56+
2057
interface ReplComponentProps {
2158
initRuntime: () => void;
2259
runtimeInitializing: boolean;
@@ -125,18 +162,7 @@ export function ReplTerminal(props: ReplComponentProps) {
125162
const onOutput = useCallback(
126163
(outputs: ReplOutput[]) => {
127164
if (terminalInstanceRef.current) {
128-
for (const output of outputs) {
129-
// 出力内容に応じて色を変える
130-
const message = String(output.message).replace(/\n/g, "\r\n");
131-
switch (output.type) {
132-
case "error":
133-
terminalInstanceRef.current.writeln(chalk.red(message));
134-
break;
135-
default:
136-
terminalInstanceRef.current.writeln(message);
137-
break;
138-
}
139-
}
165+
writeOutput(terminalInstanceRef.current, outputs, true);
140166
// 出力が終わったらプロンプトを表示
141167
updateBuffer(() => [""]);
142168
}
@@ -296,7 +322,7 @@ export function ReplTerminal(props: ReplComponentProps) {
296322
initRuntime();
297323
hideCursor(terminalInstanceRef.current);
298324
terminalInstanceRef.current.write(
299-
chalk.dim.bold.italic(
325+
systemMessageColor(
300326
"(初期化しています...しばらくお待ちください)"
301327
)
302328
);

app/terminal/terminal.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
44
import { Terminal } from "@xterm/xterm";
55
import { FitAddon } from "@xterm/addon-fit";
66
import "@xterm/xterm/css/xterm.css";
7+
import chalk from "chalk";
78

89
/**
910
* 文字列の幅を計算する。
@@ -49,6 +50,8 @@ export function showCursor(term: Terminal) {
4950
term.write("\x1b[?25h");
5051
}
5152

53+
export const systemMessageColor = chalk.blue.bold.italic;
54+
5255
interface TerminalProps {
5356
getRows?: (cols: number) => number;
5457
onReady?: () => void;

app/terminal/wandbox/wandbox.tsx

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -251,20 +251,49 @@ export function WandboxProvider({ children }: { children: ReactNode }) {
251251
}
252252
).then((res) => res.json());
253253

254-
return [
255-
...(result.compiler_output
256-
? [{ type: "stdout" as const, message: result.compiler_output }]
257-
: []),
258-
...(result.compiler_error
259-
? [{ type: "error" as const, message: result.compiler_error }]
260-
: []),
261-
...(result.program_output
262-
? [{ type: "stdout" as const, message: result.program_output }]
263-
: []),
264-
...(result.program_error
265-
? [{ type: "error" as const, message: result.program_error }]
266-
: []),
267-
] satisfies ReplOutput[];
254+
let outputs: ReplOutput[] = [];
255+
if (result.compiler_output) {
256+
outputs = outputs.concat(
257+
result.compiler_output
258+
.trim()
259+
.split("\n")
260+
.map((line) => ({ type: "stdout" as const, message: line }))
261+
);
262+
}
263+
if (result.compiler_error) {
264+
outputs = outputs.concat(
265+
result.compiler_error
266+
.trim()
267+
.split("\n")
268+
.map((line) => ({ type: "error" as const, message: line }))
269+
);
270+
}
271+
if (result.program_output) {
272+
outputs = outputs.concat(
273+
result.program_output
274+
.trim()
275+
.split("\n")
276+
.map((line) => ({ type: "stdout" as const, message: line }))
277+
);
278+
}
279+
if (result.program_error) {
280+
outputs = outputs.concat(
281+
result.program_error
282+
.trim()
283+
.split("\n")
284+
.map((line) => ({ type: "error" as const, message: line }))
285+
);
286+
}
287+
if (result.status !== "0") {
288+
outputs.push({
289+
type: "system" as const,
290+
message: `ステータス ${result.status} で異常終了しました`,
291+
});
292+
}
293+
// TODO: result.signal はいつ使われるのか?
294+
295+
console.log(outputs);
296+
return outputs;
268297
},
269298
[files, cppOptions]
270299
);

0 commit comments

Comments
 (0)