Skip to content
Draft
31 changes: 20 additions & 11 deletions app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
systemMessageColor,
useTerminal,
} from "./terminal";
import { writeOutput } from "./repl";
import { writeOutput, ReplOutput } from "./repl";
import { useEffect, useState } from "react";
import { useEmbedContext } from "./embedContext";
import { RuntimeLang, useRuntime } from "./runtime";
Expand Down Expand Up @@ -46,16 +46,25 @@ export function ExecFile(props: ExecProps) {
(async () => {
clearTerminal(terminalInstanceRef.current!);
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
const outputs = await runFiles(props.filenames, files);
clearTerminal(terminalInstanceRef.current!);
writeOutput(
terminalInstanceRef.current!,
outputs,
false,
undefined,
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
props.language
);
const outputs: ReplOutput[] = [];
let isFirstOutput = true;
await runFiles(props.filenames, files, (output) => {
outputs.push(output);
if (isFirstOutput) {
// Clear "実行中です..." message only on first output
clearTerminal(terminalInstanceRef.current!);
isFirstOutput = false;
}
// Append only the new output
writeOutput(
terminalInstanceRef.current!,
[output],
true,
undefined,
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
props.language
);
});
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
setExecResult(props.filenames.join(","), outputs);
setExecutionState("idle");
Expand Down
48 changes: 34 additions & 14 deletions app/terminal/repl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,21 +176,19 @@ export function ReplTerminal({

// ランタイムからのoutputを描画し、inputBufferをリセット
const handleOutput = useCallback(
(outputs: ReplOutput[]) => {
(output: ReplOutput) => {
if (terminalInstanceRef.current) {
writeOutput(
terminalInstanceRef.current,
outputs,
true,
[output],
false,
Comment on lines 181 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

たぶんこのフラグ変えちゃダメだとおもう

returnPrefix,
Prism,
language
);
// 出力が終わったらプロンプトを表示
updateBuffer(() => [""]);
}
},
[Prism, updateBuffer, terminalInstanceRef, returnPrefix, language]
[Prism, terminalInstanceRef, returnPrefix, language]
);

const keyHandler = useCallback(
Expand Down Expand Up @@ -220,11 +218,20 @@ export function ReplTerminal({
terminalInstanceRef.current.writeln("");
const command = inputBuffer.current.join("\n").trim();
inputBuffer.current = [];
const outputs = await runtimeMutex.runExclusive(() =>
runCommand(command)
);
handleOutput(outputs);
addReplOutput?.(terminalId, command, outputs);
const collectedOutputs: ReplOutput[] = [];
let isFirstOutput = true;
await runtimeMutex.runExclusive(async () => {
await runCommand(command, (output) => {
collectedOutputs.push(output);
handleOutput(output);
isFirstOutput = false;
});
});
if (!isFirstOutput && terminalInstanceRef.current) {
terminalInstanceRef.current.writeln("");
}
updateBuffer(() => [""]);
addReplOutput?.(terminalId, command, collectedOutputs);
}
} else if (code === 127) {
// Backspace
Expand Down Expand Up @@ -301,8 +308,14 @@ export function ReplTerminal({
updateBuffer(() => cmd.command.split("\n"));
terminalInstanceRef.current!.writeln("");
inputBuffer.current = [];
handleOutput(cmd.output);
for (const output of cmd.output) {
handleOutput(output);
}
terminalInstanceRef.current!.writeln("");
updateBuffer(() => [""]);
}
} else {
updateBuffer(() => [""]);
}
terminalInstanceRef.current!.scrollToTop();
setInitCommandState("idle");
Expand All @@ -320,7 +333,10 @@ export function ReplTerminal({
const initCommandResult: ReplCommand[] = [];
await runtimeMutex.runExclusive(async () => {
for (const cmd of initCommand!) {
const outputs = await runCommand(cmd.command);
const outputs: ReplOutput[] = [];
await runCommand(cmd.command, (output) => {
outputs.push(output);
});
initCommandResult.push({
command: cmd.command,
output: outputs,
Expand All @@ -333,7 +349,11 @@ export function ReplTerminal({
updateBuffer(() => cmd.command.split("\n"));
terminalInstanceRef.current!.writeln("");
inputBuffer.current = [];
handleOutput(cmd.output);
for (const output of cmd.output) {
handleOutput(output);
}
terminalInstanceRef.current!.writeln("");
updateBuffer(() => [""]);
}
}
updateBuffer(() => [""]);
Expand Down
10 changes: 7 additions & 3 deletions app/terminal/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ export interface RuntimeContext {
mutex?: MutexInterface;
interrupt?: () => void;
// repl
runCommand?: (command: string) => Promise<ReplOutput[]>;
runCommand?: (
command: string,
onOutput: (output: ReplOutput) => void
) => Promise<void>;
checkSyntax?: (code: string) => Promise<SyntaxStatus>;
splitReplExamples?: (content: string) => ReplCommand[];
// file
runFiles: (
filenames: string[],
files: Readonly<Record<string, string>>
) => Promise<ReplOutput[]>;
files: Readonly<Record<string, string>>,
onOutput: (output: ReplOutput) => void
) => Promise<void>;
getCommandlineStr?: (filenames: string[]) => string;
}
export interface LangConstants {
Expand Down
94 changes: 59 additions & 35 deletions app/terminal/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ export function defineTests(
if (!printCode) {
this.skip();
}
const result = await (
const outputs: any[] = [];
await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(() => runtimeRef.current[lang].runCommand!(printCode));
console.log(`${lang} REPL stdout test: `, result);
expect(result).to.be.deep.include({ type: "stdout", message: msg });
).runExclusive(() =>
runtimeRef.current[lang].runCommand!(printCode, (output) => {
outputs.push(output);
})
);
console.log(`${lang} REPL stdout test: `, outputs);
expect(outputs).to.be.deep.include({ type: "stdout", message: msg });
});

it("should preserve variables across commands", async function () {
Expand All @@ -68,14 +73,17 @@ export function defineTests(
if (!setIntVarCode || !printIntVarCode) {
this.skip();
}
const result = await (
const outputs: any[] = [];
await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(async () => {
await runtimeRef.current[lang].runCommand!(setIntVarCode);
return runtimeRef.current[lang].runCommand!(printIntVarCode);
await runtimeRef.current[lang].runCommand!(setIntVarCode, () => {});
await runtimeRef.current[lang].runCommand!(printIntVarCode, (output) => {
outputs.push(output);
});
});
console.log(`${lang} REPL variable preservation test: `, result);
expect(result).to.be.deep.include({
console.log(`${lang} REPL variable preservation test: `, outputs);
expect(outputs).to.be.deep.include({
type: "stdout",
message: value.toString(),
});
Expand All @@ -96,12 +104,17 @@ export function defineTests(
if (!errorCode) {
this.skip();
}
const result = await (
const outputs: any[] = [];
await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(() => runtimeRef.current[lang].runCommand!(errorCode));
console.log(`${lang} REPL error capture test: `, result);
).runExclusive(() =>
runtimeRef.current[lang].runCommand!(errorCode, (output) => {
outputs.push(output);
})
);
console.log(`${lang} REPL error capture test: `, outputs);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be
expect(outputs.filter((r) => r.message.includes(errorMsg))).to.not.be
.empty;
});

Expand All @@ -126,8 +139,8 @@ export function defineTests(
const runPromise = (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(async () => {
await runtimeRef.current[lang].runCommand!(setIntVarCode);
return runtimeRef.current[lang].runCommand!(infLoopCode);
await runtimeRef.current[lang].runCommand!(setIntVarCode, () => {});
await runtimeRef.current[lang].runCommand!(infLoopCode, () => {});
});
// Wait a bit to ensure the infinite loop has started
await new Promise((resolve) => setTimeout(resolve, 1000));
Expand All @@ -137,13 +150,16 @@ export function defineTests(
while (!runtimeRef.current[lang].ready) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
const result = await (
const outputs: any[] = [];
await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(() =>
runtimeRef.current[lang].runCommand!(printIntVarCode)
runtimeRef.current[lang].runCommand!(printIntVarCode, (output) => {
outputs.push(output);
})
);
console.log(`${lang} REPL interrupt recovery test: `, result);
expect(result).to.be.deep.include({ type: "stdout", message: "42" });
console.log(`${lang} REPL interrupt recovery test: `, outputs);
expect(outputs).to.be.deep.include({ type: "stdout", message: "42" });
});

it("should capture files modified by command", async function () {
Expand All @@ -162,10 +178,9 @@ export function defineTests(
if (!writeCode) {
this.skip();
}
const result = await (
await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(() => runtimeRef.current[lang].runCommand!(writeCode));
console.log(`${lang} REPL file modify test: `, result);
).runExclusive(() => runtimeRef.current[lang].runCommand!(writeCode, () => {}));
// wait for files to be updated
await new Promise((resolve) => setTimeout(resolve, 100));
expect(filesRef.current[targetFile]).to.equal(msg);
Expand All @@ -191,11 +206,14 @@ export function defineTests(
if (!filename || !code) {
this.skip();
}
const result = await runtimeRef.current[lang].runFiles([filename], {
const outputs: any[] = [];
await runtimeRef.current[lang].runFiles([filename], {
[filename]: code,
}, (output) => {
outputs.push(output);
});
console.log(`${lang} single file stdout test: `, result);
expect(result).to.be.deep.include({ type: "stdout", message: msg });
console.log(`${lang} single file stdout test: `, outputs);
expect(outputs).to.be.deep.include({ type: "stdout", message: msg });
});

it("should capture errors", async function () {
Expand All @@ -220,12 +238,15 @@ export function defineTests(
if (!filename || !code) {
this.skip();
}
const result = await runtimeRef.current[lang].runFiles([filename], {
const outputs: any[] = [];
await runtimeRef.current[lang].runFiles([filename], {
[filename]: code,
}, (output) => {
outputs.push(output);
});
console.log(`${lang} single file error capture test: `, result);
console.log(`${lang} single file error capture test: `, outputs);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be
expect(outputs.filter((r) => r.message.includes(errorMsg))).to.not.be
.empty;
});

Expand Down Expand Up @@ -276,12 +297,16 @@ export function defineTests(
if (!codes || !execFiles) {
this.skip();
}
const result = await runtimeRef.current[lang].runFiles(
const outputs: any[] = [];
await runtimeRef.current[lang].runFiles(
execFiles,
codes
codes,
(output) => {
outputs.push(output);
}
);
console.log(`${lang} multifile stdout test: `, result);
expect(result).to.be.deep.include({ type: "stdout", message: msg });
console.log(`${lang} multifile stdout test: `, outputs);
expect(outputs).to.be.deep.include({ type: "stdout", message: msg });
});

it("should capture files modified by script", async function () {
Expand All @@ -306,10 +331,9 @@ export function defineTests(
if (!filename || !code) {
this.skip();
}
const result = await runtimeRef.current[lang].runFiles([filename], {
await runtimeRef.current[lang].runFiles([filename], {
[filename]: code,
});
console.log(`${lang} file modify test: `, result);
}, () => {});
// wait for files to be updated
await new Promise((resolve) => setTimeout(resolve, 100));
expect(filesRef.current[targetFile]).to.equal(msg);
Expand Down
24 changes: 12 additions & 12 deletions app/terminal/typescript/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,25 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {

const { writeFile } = useEmbedContext();
const runFiles = useCallback(
async (filenames: string[], files: Readonly<Record<string, string>>) => {
async (
filenames: string[],
files: Readonly<Record<string, string>>,
onOutput: (output: ReplOutput) => void
) => {
if (tsEnv === null || typeof window === "undefined") {
return [
{ type: "error" as const, message: "TypeScript is not ready yet." },
];
onOutput({ type: "error", message: "TypeScript is not ready yet." });
return;
} else {
for (const [filename, content] of Object.entries(files)) {
tsEnv.createFile(filename, content);
}

const outputs: ReplOutput[] = [];

const ts = await import("typescript");

for (const diagnostic of tsEnv.languageService.getSyntacticDiagnostics(
filenames[0]
)) {
outputs.push({
onOutput({
type: "error",
message: ts.formatDiagnosticsWithColorAndContext([diagnostic], {
getCurrentDirectory: () => "",
Expand All @@ -121,7 +122,7 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
for (const diagnostic of tsEnv.languageService.getSemanticDiagnostics(
filenames[0]
)) {
outputs.push({
onOutput({
type: "error",
message: ts.formatDiagnosticsWithColorAndContext([diagnostic], {
getCurrentDirectory: () => "",
Expand All @@ -143,12 +144,11 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
}

console.log(emitOutput);
const jsOutputs = jsEval.runFiles(
await jsEval.runFiles(
[emitOutput.outputFiles[0].name],
files
files,
onOutput
);

return outputs.concat(await jsOutputs);
}
},
[tsEnv, writeFile, jsEval]
Expand Down
Loading
Loading