Skip to content

Commit 85ab8df

Browse files
authored
Merge pull request #158 from ut-code/repl-async-output
workerの実行結果の非同期出力
2 parents ba120a8 + 442d856 commit 85ab8df

6 files changed

Lines changed: 93 additions & 37 deletions

File tree

app/terminal/embedContext.tsx

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ interface IEmbedContext {
3232
) => Promise<Readonly<Record<Filename, string>>>;
3333

3434
replOutputs: Readonly<Record<TerminalId, ReplCommand[]>>;
35+
addReplCommand: (terminalId: TerminalId, command: string) => string;
3536
addReplOutput: (
3637
terminalId: TerminalId,
37-
command: string,
38-
output: ReplOutput[]
38+
commandId: string,
39+
output: ReplOutput
3940
) => void;
4041

4142
execResults: Readonly<Record<Filename, ReplOutput[]>>;
42-
setExecResult: (filename: Filename, output: ReplOutput[]) => void;
43+
clearExecResult: (filename: Filename) => void;
44+
addExecOutput: (filename: Filename, output: ReplOutput) => void;
4345
}
4446
const EmbedContext = createContext<IEmbedContext>(null!);
4547

@@ -63,6 +65,10 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
6365
const [replOutputs, setReplOutputs] = useState<
6466
Record<TerminalId, ReplCommand[]>
6567
>({});
68+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
69+
const [commandIdCounters, setCommandIdCounters] = useState<
70+
Record<TerminalId, number>
71+
>({});
6672
const [execResults, setExecResults] = useState<
6773
Record<Filename, ReplOutput[]>
6874
>({});
@@ -71,6 +77,7 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
7177
if (pathname && pathname !== currentPathname) {
7278
setCurrentPathname(pathname);
7379
setReplOutputs({});
80+
setCommandIdCounters({});
7481
setExecResults({});
7582
}
7683
}, [pathname, currentPathname]);
@@ -100,26 +107,70 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
100107
},
101108
[pathname]
102109
);
103-
const addReplOutput = useCallback(
104-
(terminalId: TerminalId, command: string, output: ReplOutput[]) =>
110+
const addReplCommand = useCallback(
111+
(terminalId: TerminalId, command: string): string => {
112+
let commandId = "";
113+
setCommandIdCounters((counters) => {
114+
const newCounters = { ...counters };
115+
const currentCount = newCounters[terminalId] ?? 0;
116+
commandId = String(currentCount);
117+
newCounters[terminalId] = currentCount + 1;
118+
return newCounters;
119+
});
105120
setReplOutputs((outs) => {
106121
outs = { ...outs };
107122
if (!(terminalId in outs)) {
108123
outs[terminalId] = [];
109124
}
110125
outs[terminalId] = [
111126
...outs[terminalId],
112-
{ command: command, output: output },
127+
{ command: command, output: [], commandId },
113128
];
114129
return outs;
130+
});
131+
return commandId;
132+
},
133+
[]
134+
);
135+
const addReplOutput = useCallback(
136+
(terminalId: TerminalId, commandId: string, output: ReplOutput) =>
137+
setReplOutputs((outs) => {
138+
outs = { ...outs };
139+
if (terminalId in outs) {
140+
outs[terminalId] = [...outs[terminalId]];
141+
// Find the command by commandId
142+
const commandIndex = outs[terminalId].findIndex(
143+
(cmd) => cmd.commandId === commandId
144+
);
145+
if (commandIndex >= 0) {
146+
const command = outs[terminalId][commandIndex];
147+
outs[terminalId][commandIndex] = {
148+
...command,
149+
output: [...command.output, output],
150+
};
151+
}
152+
}
153+
return outs;
154+
}),
155+
[]
156+
);
157+
const clearExecResult = useCallback(
158+
(filename: Filename) =>
159+
setExecResults((results) => {
160+
results = { ...results };
161+
results[filename] = [];
162+
return results;
115163
}),
116164
[]
117165
);
118-
const setExecResult = useCallback(
119-
(filename: Filename, output: ReplOutput[]) =>
166+
const addExecOutput = useCallback(
167+
(filename: Filename, output: ReplOutput) =>
120168
setExecResults((results) => {
121169
results = { ...results };
122-
results[filename] = output;
170+
if (!(filename in results)) {
171+
results[filename] = [];
172+
}
173+
results[filename] = [...results[filename], output];
123174
return results;
124175
}),
125176
[]
@@ -131,9 +182,11 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
131182
files: files[pathname] || {},
132183
writeFile,
133184
replOutputs,
185+
addReplCommand,
134186
addReplOutput,
135187
execResults,
136-
setExecResult,
188+
clearExecResult,
189+
addExecOutput,
137190
}}
138191
>
139192
{children}

app/terminal/exec.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
systemMessageColor,
88
useTerminal,
99
} from "./terminal";
10-
import { writeOutput, ReplOutput } from "./repl";
10+
import { writeOutput } from "./repl";
1111
import { useEffect, useState } from "react";
1212
import { useEmbedContext } from "./embedContext";
1313
import { RuntimeLang, useRuntime } from "./runtime";
@@ -32,7 +32,7 @@ export function ExecFile(props: ExecProps) {
3232
}
3333
},
3434
});
35-
const { files, setExecResult } = useEmbedContext();
35+
const { files, clearExecResult, addExecOutput } = useEmbedContext();
3636

3737
const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);
3838

@@ -46,10 +46,12 @@ export function ExecFile(props: ExecProps) {
4646
(async () => {
4747
clearTerminal(terminalInstanceRef.current!);
4848
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
49-
const outputs: ReplOutput[] = [];
49+
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
50+
const filenameKey = props.filenames.join(",");
51+
clearExecResult(filenameKey);
5052
let isFirstOutput = true;
5153
await runFiles(props.filenames, files, (output) => {
52-
outputs.push(output);
54+
addExecOutput(filenameKey, output);
5355
if (isFirstOutput) {
5456
// Clear "実行中です..." message only on first output
5557
clearTerminal(terminalInstanceRef.current!);
@@ -64,8 +66,6 @@ export function ExecFile(props: ExecProps) {
6466
props.language
6567
);
6668
});
67-
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
68-
setExecResult(props.filenames.join(","), outputs);
6969
setExecutionState("idle");
7070
})();
7171
}
@@ -74,7 +74,8 @@ export function ExecFile(props: ExecProps) {
7474
ready,
7575
props.filenames,
7676
runFiles,
77-
setExecResult,
77+
clearExecResult,
78+
addExecOutput,
7879
terminalInstanceRef,
7980
props.language,
8081
files,

app/terminal/repl.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface ReplOutput {
3131
export interface ReplCommand {
3232
command: string;
3333
output: ReplOutput[];
34+
commandId?: string; // Optional for backward compatibility
3435
}
3536
export type SyntaxStatus = "complete" | "incomplete" | "invalid"; // 構文チェックの結果
3637

@@ -80,7 +81,7 @@ export function ReplTerminal({
8081
language,
8182
initContent,
8283
}: ReplComponentProps) {
83-
const { addReplOutput } = useEmbedContext();
84+
const { addReplCommand, addReplOutput } = useEmbedContext();
8485

8586
const [Prism, setPrism] = useState<typeof import("prismjs") | null>(null);
8687
useEffect(() => {
@@ -130,7 +131,7 @@ export function ReplTerminal({
130131

131132
// inputBufferを更新し、画面に描画する
132133
const updateBuffer = useCallback(
133-
(newBuffer: () => string[]) => {
134+
(newBuffer: (() => string[]) | null, insertBefore?: () => void) => {
134135
if (terminalInstanceRef.current) {
135136
hideCursor(terminalInstanceRef.current);
136137
// バッファの行数分カーソルを戻す
@@ -142,8 +143,12 @@ export function ReplTerminal({
142143
terminalInstanceRef.current.write("\r");
143144
// バッファの内容をクリア
144145
terminalInstanceRef.current.write("\x1b[0J");
145-
// 新しいバッファの内容を表示
146-
inputBuffer.current = newBuffer();
146+
// バッファの前に追加で出力する内容(前のコマンドの出力)があればここで書き込む
147+
insertBefore?.();
148+
// 新しいバッファの内容を表示、nullなら現状維持
149+
if (newBuffer) {
150+
inputBuffer.current = newBuffer();
151+
}
147152
for (let i = 0; i < inputBuffer.current.length; i++) {
148153
terminalInstanceRef.current.write(
149154
(i === 0 ? prompt : (promptMore ?? prompt)) ?? "> "
@@ -213,15 +218,23 @@ export function ReplTerminal({
213218
terminalInstanceRef.current.writeln("");
214219
const command = inputBuffer.current.join("\n").trim();
215220
inputBuffer.current = [];
216-
const collectedOutputs: ReplOutput[] = [];
221+
const commandId = addReplCommand(terminalId, command);
222+
let executionDone = false;
217223
await runtimeMutex.runExclusive(async () => {
218224
await runCommand(command, (output) => {
219-
collectedOutputs.push(output);
220-
handleOutput(output);
225+
if (executionDone) {
226+
// すでに完了していて次のコマンドのプロンプトが出ている場合、その前に挿入
227+
updateBuffer(null, () => {
228+
handleOutput(output);
229+
});
230+
} else {
231+
handleOutput(output);
232+
}
233+
addReplOutput(terminalId, commandId, output);
221234
});
222235
});
236+
executionDone = true;
223237
updateBuffer(() => [""]);
224-
addReplOutput?.(terminalId, command, collectedOutputs);
225238
}
226239
} else if (code === 127) {
227240
// Backspace
@@ -265,6 +278,7 @@ export function ReplTerminal({
265278
runCommand,
266279
handleOutput,
267280
tabSize,
281+
addReplCommand,
268282
addReplOutput,
269283
terminalId,
270284
terminalInstanceRef,

app/terminal/worker/jsEval.worker.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ async function runCode(
9696
message: `${String(e)}`,
9797
});
9898
}
99-
} finally {
100-
currentOutputCallback = null;
10199
}
102100

103101
return { updatedFiles: {} as Record<string, string> };
@@ -126,8 +124,6 @@ function runFile(
126124
message: `${String(e)}`,
127125
});
128126
}
129-
} finally {
130-
currentOutputCallback = null;
131127
}
132128

133129
return { updatedFiles: {} as Record<string, string> };

app/terminal/worker/pyodide.worker.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ async function runCode(
107107
message: `予期せぬエラー: ${String(e).trim()}`,
108108
});
109109
}
110-
} finally {
111-
currentOutputCallback = null;
112110
}
113111

114112
const updatedFiles = readAllFiles();
@@ -165,8 +163,6 @@ async function runFile(
165163
message: `予期せぬエラー: ${String(e).trim()}`,
166164
});
167165
}
168-
} finally {
169-
currentOutputCallback = null;
170166
}
171167

172168
const updatedFiles = readAllFiles();

app/terminal/worker/ruby.worker.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,6 @@ async function runCode(
137137
type: "error",
138138
message: formatRubyError(e, false),
139139
});
140-
} finally {
141-
currentOutputCallback = null;
142140
}
143141

144142
const updatedFiles = readAllFiles();
@@ -189,8 +187,6 @@ async function runFile(
189187
type: "error",
190188
message: formatRubyError(e, true),
191189
});
192-
} finally {
193-
currentOutputCallback = null;
194190
}
195191

196192
const updatedFiles = readAllFiles();

0 commit comments

Comments
 (0)