diff --git a/app/terminal/embedContext.tsx b/app/terminal/embedContext.tsx index 53138d3..bc447f3 100644 --- a/app/terminal/embedContext.tsx +++ b/app/terminal/embedContext.tsx @@ -32,14 +32,16 @@ interface IEmbedContext { ) => Promise>>; replOutputs: Readonly>; + addReplCommand: (terminalId: TerminalId, command: string) => string; addReplOutput: ( terminalId: TerminalId, - command: string, - output: ReplOutput[] + commandId: string, + output: ReplOutput ) => void; execResults: Readonly>; - setExecResult: (filename: Filename, output: ReplOutput[]) => void; + clearExecResult: (filename: Filename) => void; + addExecOutput: (filename: Filename, output: ReplOutput) => void; } const EmbedContext = createContext(null!); @@ -63,6 +65,10 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { const [replOutputs, setReplOutputs] = useState< Record >({}); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [commandIdCounters, setCommandIdCounters] = useState< + Record + >({}); const [execResults, setExecResults] = useState< Record >({}); @@ -71,6 +77,7 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { if (pathname && pathname !== currentPathname) { setCurrentPathname(pathname); setReplOutputs({}); + setCommandIdCounters({}); setExecResults({}); } }, [pathname, currentPathname]); @@ -100,8 +107,16 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { }, [pathname] ); - const addReplOutput = useCallback( - (terminalId: TerminalId, command: string, output: ReplOutput[]) => + const addReplCommand = useCallback( + (terminalId: TerminalId, command: string): string => { + let commandId = ""; + setCommandIdCounters((counters) => { + const newCounters = { ...counters }; + const currentCount = newCounters[terminalId] ?? 0; + commandId = String(currentCount); + newCounters[terminalId] = currentCount + 1; + return newCounters; + }); setReplOutputs((outs) => { outs = { ...outs }; if (!(terminalId in outs)) { @@ -109,17 +124,53 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { } outs[terminalId] = [ ...outs[terminalId], - { command: command, output: output }, + { command: command, output: [], commandId }, ]; return outs; + }); + return commandId; + }, + [] + ); + const addReplOutput = useCallback( + (terminalId: TerminalId, commandId: string, output: ReplOutput) => + setReplOutputs((outs) => { + outs = { ...outs }; + if (terminalId in outs) { + outs[terminalId] = [...outs[terminalId]]; + // Find the command by commandId + const commandIndex = outs[terminalId].findIndex( + (cmd) => cmd.commandId === commandId + ); + if (commandIndex >= 0) { + const command = outs[terminalId][commandIndex]; + outs[terminalId][commandIndex] = { + ...command, + output: [...command.output, output], + }; + } + } + return outs; + }), + [] + ); + const clearExecResult = useCallback( + (filename: Filename) => + setExecResults((results) => { + results = { ...results }; + results[filename] = []; + return results; }), [] ); - const setExecResult = useCallback( - (filename: Filename, output: ReplOutput[]) => + const addExecOutput = useCallback( + (filename: Filename, output: ReplOutput) => setExecResults((results) => { results = { ...results }; - results[filename] = output; + if (!(filename in results)) { + results[filename] = []; + } + results[filename] = [...results[filename], output]; return results; }), [] @@ -131,9 +182,11 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) { files: files[pathname] || {}, writeFile, replOutputs, + addReplCommand, addReplOutput, execResults, - setExecResult, + clearExecResult, + addExecOutput, }} > {children} diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index fd7110e..e757319 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -7,7 +7,7 @@ import { systemMessageColor, useTerminal, } from "./terminal"; -import { writeOutput, ReplOutput } from "./repl"; +import { writeOutput } from "./repl"; import { useEffect, useState } from "react"; import { useEmbedContext } from "./embedContext"; import { RuntimeLang, useRuntime } from "./runtime"; @@ -32,7 +32,7 @@ export function ExecFile(props: ExecProps) { } }, }); - const { files, setExecResult } = useEmbedContext(); + const { files, clearExecResult, addExecOutput } = useEmbedContext(); const { ready, runFiles, getCommandlineStr } = useRuntime(props.language); @@ -46,10 +46,12 @@ export function ExecFile(props: ExecProps) { (async () => { clearTerminal(terminalInstanceRef.current!); terminalInstanceRef.current!.write(systemMessageColor("実行中です...")); - const outputs: ReplOutput[] = []; + // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる + const filenameKey = props.filenames.join(","); + clearExecResult(filenameKey); let isFirstOutput = true; await runFiles(props.filenames, files, (output) => { - outputs.push(output); + addExecOutput(filenameKey, output); if (isFirstOutput) { // Clear "実行中です..." message only on first output clearTerminal(terminalInstanceRef.current!); @@ -64,8 +66,6 @@ export function ExecFile(props: ExecProps) { props.language ); }); - // TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる - setExecResult(props.filenames.join(","), outputs); setExecutionState("idle"); })(); } @@ -74,7 +74,8 @@ export function ExecFile(props: ExecProps) { ready, props.filenames, runFiles, - setExecResult, + clearExecResult, + addExecOutput, terminalInstanceRef, props.language, files, diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index d34bcf0..6c5eca7 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -31,6 +31,7 @@ export interface ReplOutput { export interface ReplCommand { command: string; output: ReplOutput[]; + commandId?: string; // Optional for backward compatibility } export type SyntaxStatus = "complete" | "incomplete" | "invalid"; // 構文チェックの結果 @@ -80,7 +81,7 @@ export function ReplTerminal({ language, initContent, }: ReplComponentProps) { - const { addReplOutput } = useEmbedContext(); + const { addReplCommand, addReplOutput } = useEmbedContext(); const [Prism, setPrism] = useState(null); useEffect(() => { @@ -130,7 +131,7 @@ export function ReplTerminal({ // inputBufferを更新し、画面に描画する const updateBuffer = useCallback( - (newBuffer: () => string[]) => { + (newBuffer: (() => string[]) | null, insertBefore?: () => void) => { if (terminalInstanceRef.current) { hideCursor(terminalInstanceRef.current); // バッファの行数分カーソルを戻す @@ -142,8 +143,12 @@ export function ReplTerminal({ terminalInstanceRef.current.write("\r"); // バッファの内容をクリア terminalInstanceRef.current.write("\x1b[0J"); - // 新しいバッファの内容を表示 - inputBuffer.current = newBuffer(); + // バッファの前に追加で出力する内容(前のコマンドの出力)があればここで書き込む + insertBefore?.(); + // 新しいバッファの内容を表示、nullなら現状維持 + if (newBuffer) { + inputBuffer.current = newBuffer(); + } for (let i = 0; i < inputBuffer.current.length; i++) { terminalInstanceRef.current.write( (i === 0 ? prompt : (promptMore ?? prompt)) ?? "> " @@ -213,15 +218,23 @@ export function ReplTerminal({ terminalInstanceRef.current.writeln(""); const command = inputBuffer.current.join("\n").trim(); inputBuffer.current = []; - const collectedOutputs: ReplOutput[] = []; + const commandId = addReplCommand(terminalId, command); + let executionDone = false; await runtimeMutex.runExclusive(async () => { await runCommand(command, (output) => { - collectedOutputs.push(output); - handleOutput(output); + if (executionDone) { + // すでに完了していて次のコマンドのプロンプトが出ている場合、その前に挿入 + updateBuffer(null, () => { + handleOutput(output); + }); + } else { + handleOutput(output); + } + addReplOutput(terminalId, commandId, output); }); }); + executionDone = true; updateBuffer(() => [""]); - addReplOutput?.(terminalId, command, collectedOutputs); } } else if (code === 127) { // Backspace @@ -265,6 +278,7 @@ export function ReplTerminal({ runCommand, handleOutput, tabSize, + addReplCommand, addReplOutput, terminalId, terminalInstanceRef, diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index 64e2302..2f777ea 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -96,8 +96,6 @@ async function runCode( message: `${String(e)}`, }); } - } finally { - currentOutputCallback = null; } return { updatedFiles: {} as Record }; @@ -126,8 +124,6 @@ function runFile( message: `${String(e)}`, }); } - } finally { - currentOutputCallback = null; } return { updatedFiles: {} as Record }; diff --git a/app/terminal/worker/pyodide.worker.ts b/app/terminal/worker/pyodide.worker.ts index fbfbe7e..9464b0c 100644 --- a/app/terminal/worker/pyodide.worker.ts +++ b/app/terminal/worker/pyodide.worker.ts @@ -107,8 +107,6 @@ async function runCode( message: `予期せぬエラー: ${String(e).trim()}`, }); } - } finally { - currentOutputCallback = null; } const updatedFiles = readAllFiles(); @@ -165,8 +163,6 @@ async function runFile( message: `予期せぬエラー: ${String(e).trim()}`, }); } - } finally { - currentOutputCallback = null; } const updatedFiles = readAllFiles(); diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index af6779d..121a2ed 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -137,8 +137,6 @@ async function runCode( type: "error", message: formatRubyError(e, false), }); - } finally { - currentOutputCallback = null; } const updatedFiles = readAllFiles(); @@ -189,8 +187,6 @@ async function runFile( type: "error", message: formatRubyError(e, true), }); - } finally { - currentOutputCallback = null; } const updatedFiles = readAllFiles();