+
-
+
{getCommandlineStr?.(props.filenames)}
+
+ {/*なぜかわからないがz-1がないと後ろに隠れてしまう*/}
+
+ ブラウザ上で動作する
+
+ {runtimeInfo?.prettyLangName || props.language}
+
+ {runtimeInfo?.version && (
+ {runtimeInfo?.version}
+ )}
+ の実行環境です。
+
+ 左上の実行ボタンを押して、このページ内の
+ {props.filenames.map((fname) => (
+
+ {fname}
+
+ ))}
+ に書かれている内容を実行します。
+
+
+
-
+
{/*
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
@@ -130,10 +175,10 @@ export function ExecFile(props: ExecProps) {
)}
ref={terminalRef}
/>
+ {executionState !== "idle" && (
+
+ )}
- {executionState !== "idle" && (
-
- )}
);
}
diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx
index 6c5eca7..286d35e 100644
--- a/app/terminal/repl.tsx
+++ b/app/terminal/repl.tsx
@@ -16,6 +16,7 @@ import type { Terminal } from "@xterm/xterm";
import { useEmbedContext } from "./embedContext";
import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime";
import clsx from "clsx";
+import { InlineCode } from "@/[docs_id]/markdown";
export type ReplOutputType =
| "stdout"
@@ -97,6 +98,7 @@ export function ReplTerminal({
runCommand,
checkSyntax,
splitReplExamples,
+ runtimeInfo,
} = useRuntime(language);
const { tabSize, prompt, promptMore, returnPrefix } = langConstants(language);
if (!prompt) {
@@ -129,6 +131,10 @@ export function ReplTerminal({
// REPLのユーザー入力
const inputBuffer = useRef
([]);
+ const [executionState, setExecutionState] = useState<"idle" | "executing">(
+ "idle"
+ );
+
// inputBufferを更新し、画面に描画する
const updateBuffer = useCallback(
(newBuffer: (() => string[]) | null, insertBefore?: () => void) => {
@@ -219,6 +225,7 @@ export function ReplTerminal({
const command = inputBuffer.current.join("\n").trim();
inputBuffer.current = [];
const commandId = addReplCommand(terminalId, command);
+ setExecutionState("executing");
let executionDone = false;
await runtimeMutex.runExclusive(async () => {
await runCommand(command, (output) => {
@@ -233,6 +240,7 @@ export function ReplTerminal({
addReplOutput(terminalId, commandId, output);
});
});
+ setExecutionState("idle");
executionDone = true;
updateBuffer(() => [""]);
}
@@ -378,51 +386,109 @@ export function ReplTerminal({
]);
return (
-
+
+
+
+
+ {runtimeInfo?.prettyLangName || language} 実行環境
+
+
+
+ ブラウザ上で動作する
+
+ {runtimeInfo?.prettyLangName || language}
+
+ {runtimeInfo?.version && (
+ {runtimeInfo?.version}
+ )}
+ のREPL実行環境です。
+
+ プロンプト ({prompt?.trimEnd()})
+ の後にコマンドを入力し、
+ Enter
+ キーで実行します。
+
+ Ctrl+
+ C
+ または左上の停止ボタンで実行中のコマンドを中断できます。
+
+
+
+
{/*
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
*/}
-
- {initContent + "\n\n"}
-
- {terminalInstanceRef.current &&
- termReady &&
- initCommandState === "idle" && (
-
{
- if (!runtimeReady) {
- hideCursor(terminalInstanceRef.current!);
- terminalInstanceRef.current!.write(
- systemMessageColor(
- "(初期化しています...しばらくお待ちください)"
- )
- );
- terminalInstanceRef.current!.focus();
- }
- setInitCommandState("triggered");
- }}
- />
- )}
- {(initCommandState === "triggered" ||
- initCommandState === "executing") && (
-
- )}
-
+
+ {initContent + "\n\n"}
+
+ {terminalInstanceRef.current &&
+ termReady &&
+ initCommandState === "idle" && (
+
{
+ if (!runtimeReady) {
+ hideCursor(terminalInstanceRef.current!);
+ terminalInstanceRef.current!.write(
+ systemMessageColor(
+ "(初期化しています...しばらくお待ちください)"
+ )
+ );
+ terminalInstanceRef.current!.focus();
+ }
+ setInitCommandState("triggered");
+ }}
+ />
+ )}
+ {(initCommandState === "triggered" ||
+ initCommandState === "executing") && (
+
)}
- ref={terminalRef}
- />
+
+
);
}
diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx
index d5e649d..90c29cb 100644
--- a/app/terminal/runtime.tsx
+++ b/app/terminal/runtime.tsx
@@ -37,6 +37,11 @@ export interface RuntimeContext {
onOutput: (output: ReplOutput) => void
) => Promise
;
getCommandlineStr?: (filenames: string[]) => string;
+ runtimeInfo?: RuntimeInfo;
+}
+export interface RuntimeInfo {
+ prettyLangName: string;
+ version?: string;
}
export interface LangConstants {
tabSize: number;
diff --git a/app/terminal/typescript/runtime.tsx b/app/terminal/typescript/runtime.tsx
index b1a747d..8637b00 100644
--- a/app/terminal/typescript/runtime.tsx
+++ b/app/terminal/typescript/runtime.tsx
@@ -8,11 +8,12 @@ import {
useCallback,
useContext,
useEffect,
+ useMemo,
useState,
} from "react";
import { useEmbedContext } from "../embedContext";
import { ReplOutput } from "../repl";
-import { RuntimeContext } from "../runtime";
+import { RuntimeContext, RuntimeInfo } from "../runtime";
export const compilerOptions: CompilerOptions = {
lib: ["ESNext", "WebWorker"],
@@ -23,9 +24,11 @@ export const compilerOptions: CompilerOptions = {
const TypeScriptContext = createContext<{
init: () => void;
tsEnv: VirtualTypeScriptEnvironment | null;
+ tsVersion?: string;
}>({ init: () => undefined, tsEnv: null });
export function TypeScriptProvider({ children }: { children: ReactNode }) {
const [tsEnv, setTSEnv] = useState(null);
+ const [tsVersion, setTSVersion] = useState(undefined);
const [doInit, setDoInit] = useState(false);
const init = useCallback(() => setDoInit(true), []);
useEffect(() => {
@@ -68,6 +71,7 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
compilerOptions
);
setTSEnv(env);
+ setTSVersion(ts.version);
})();
return () => {
abortController.abort();
@@ -75,14 +79,14 @@ export function TypeScriptProvider({ children }: { children: ReactNode }) {
}
}, [tsEnv, setTSEnv, doInit]);
return (
-
+
{children}
);
}
export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
- const { init: tsInit, tsEnv } = useContext(TypeScriptContext);
+ const { init: tsInit, tsEnv, tsVersion } = useContext(TypeScriptContext);
const { init: jsInit } = jsEval;
const init = useCallback(() => {
tsInit();
@@ -153,11 +157,20 @@ export function useTypeScript(jsEval: RuntimeContext): RuntimeContext {
},
[tsEnv, writeFile, jsEval]
);
+
+ const runtimeInfo = useMemo(
+ () => ({
+ prettyLangName: "TypeScript",
+ version: tsVersion,
+ }),
+ [tsVersion]
+ );
return {
init,
ready: tsEnv !== null,
runFiles,
getCommandlineStr,
+ runtimeInfo,
};
}
diff --git a/app/terminal/wandbox/api.ts b/app/terminal/wandbox/api.ts
index 82059f7..6691b66 100644
--- a/app/terminal/wandbox/api.ts
+++ b/app/terminal/wandbox/api.ts
@@ -1,5 +1,6 @@
import { type Fetcher } from "swr";
import { type ReplOutput } from "../repl";
+import { RuntimeInfo } from "../runtime";
const WANDBOX = "https://wandbox.org";
// https://github.com/melpon/wandbox/blob/ajax/kennel2/API.rst <- 古いけど、説明と例がある
@@ -96,7 +97,7 @@ export const compilerInfoFetcher: Fetcher = () =>
(res) => res.json() as Promise
);
-export interface SelectedCompiler {
+export interface SelectedCompiler extends RuntimeInfo {
compilerName: string;
compilerOptions: string[];
compilerOptionsRaw: string[];
diff --git a/app/terminal/wandbox/cpp.ts b/app/terminal/wandbox/cpp.ts
index 7b6bdb4..db46324 100644
--- a/app/terminal/wandbox/cpp.ts
+++ b/app/terminal/wandbox/cpp.ts
@@ -20,6 +20,8 @@ export function selectCppCompiler(
compilerOptions: [],
compilerOptionsRaw: [],
getCommandlineStr: () => "",
+ prettyLangName: "GCC",
+ version: selectedCompiler.version,
};
const commandline: string[] = ["g++"]; // selectedCompiler["display-compile-command"]
diff --git a/app/terminal/wandbox/runtime.tsx b/app/terminal/wandbox/runtime.tsx
index 9672062..c7d069b 100644
--- a/app/terminal/wandbox/runtime.tsx
+++ b/app/terminal/wandbox/runtime.tsx
@@ -10,7 +10,7 @@ import {
import useSWR from "swr";
import { compilerInfoFetcher, SelectedCompiler } from "./api";
import { cppRunFiles, selectCppCompiler } from "./cpp";
-import { RuntimeContext, RuntimeLang } from "../runtime";
+import { RuntimeContext, RuntimeInfo, RuntimeLang } from "../runtime";
import { ReplOutput } from "../repl";
import { rustRunFiles, selectRustCompiler } from "./rust";
@@ -28,6 +28,7 @@ interface IWandboxContext {
files: Readonly>,
onOutput: (output: ReplOutput) => void
) => Promise;
+ runtimeInfo: Record | undefined,
}
const WandboxContext = createContext(null!);
@@ -96,6 +97,7 @@ export function WandboxProvider({ children }: { children: ReactNode }) {
ready,
getCommandlineStrWithLang,
runFilesWithLang,
+ runtimeInfo: selectedCompiler,
}}
>
{children}
@@ -124,5 +126,6 @@ export function useWandbox(lang: WandboxLang): RuntimeContext {
ready: context.ready,
runFiles,
getCommandlineStr,
+ runtimeInfo: context.runtimeInfo?.[lang],
};
}
diff --git a/app/terminal/wandbox/rust.ts b/app/terminal/wandbox/rust.ts
index d88d30c..0d3ab26 100644
--- a/app/terminal/wandbox/rust.ts
+++ b/app/terminal/wandbox/rust.ts
@@ -24,6 +24,8 @@ export function selectRustCompiler(
"&&",
"./" + filenames[0].replace(/\.rs$/, ""),
].join(" "),
+ prettyLangName: "Rust",
+ version: selectedCompiler.version,
};
}
@@ -37,83 +39,92 @@ export async function rustRunFiles(
const STACK_FRAME_PATTERN = /^\s*\d+:/;
const LOCATION_PATTERN = /^\s*at .\//;
const SYSTEM_CODE_PATTERN = /^\s*at .\/prog.rs/;
-
+
// Track state for processing panic traces
let inPanicHook = false;
let foundBacktraceHeader = false;
const traceLines: string[] = [];
const mainModule = filenames[0].replace(/\.rs$/, "");
- await compileAndRun({
- ...options,
- // メインファイルでmod宣言したものをこちらに移す
- code:
- [...(files[filenames[0]]?.matchAll(/mod\s+\w+\s*;/g) ?? [])].reduce(
- (prev, m) => prev + `${m}\n`,
- ""
- ) + prog_rs.replaceAll("__user_main_module__", mainModule),
- codes: {
- ...files,
- // メインファイルのみ:
- // main()を強制的にpubに書き換え、
- // mod foo; を use super::foo; に書き換える
- [filenames[0]]: files[filenames[0]]
- ?.replace(/(?:pub\s+)?(fn\s+main\s*\()/g, "pub $1")
- .replaceAll(/mod\s+(\w+)\s*;/g, "use super::$1;"),
+ await compileAndRun(
+ {
+ ...options,
+ // メインファイルでmod宣言したものをこちらに移す
+ code:
+ [...(files[filenames[0]]?.matchAll(/mod\s+\w+\s*;/g) ?? [])].reduce(
+ (prev, m) => prev + `${m}\n`,
+ ""
+ ) + prog_rs.replaceAll("__user_main_module__", mainModule),
+ codes: {
+ ...files,
+ // メインファイルのみ:
+ // main()を強制的にpubに書き換え、
+ // mod foo; を use super::foo; に書き換える
+ [filenames[0]]: files[filenames[0]]
+ ?.replace(/(?:pub\s+)?(fn\s+main\s*\()/g, "pub $1")
+ .replaceAll(/mod\s+(\w+)\s*;/g, "use super::$1;"),
+ },
},
- }, (event) => {
- const { ndjsonType, output } = event;
-
- // Check for panic hook marker
- if (ndjsonType === "StdErr" && output.message === "#!my_code_panic_hook:") {
- inPanicHook = true;
- return;
- }
-
- if (inPanicHook && ndjsonType === "StdErr") {
- // Check for stack backtrace header
- if (output.message === "stack backtrace:") {
- foundBacktraceHeader = true;
- onOutput({
- type: "trace",
- message: "Stack trace (filtered):",
- });
+ (event) => {
+ const { ndjsonType, output } = event;
+
+ // Check for panic hook marker
+ if (
+ ndjsonType === "StdErr" &&
+ output.message === "#!my_code_panic_hook:"
+ ) {
+ inPanicHook = true;
return;
}
-
- if (foundBacktraceHeader) {
- // Process stack trace lines
- // Look for pattern: " N: ..." followed by " at ./file.rs:line"
- if (STACK_FRAME_PATTERN.test(output.message)) {
- traceLines.push(output.message);
- } else if (LOCATION_PATTERN.test(output.message)) {
- if (traceLines.length > 0) {
- // Check if this is user code (not prog.rs)
- if (!SYSTEM_CODE_PATTERN.test(output.message)) {
- onOutput({
- type: "trace",
- message: traceLines[traceLines.length - 1].replace("prog::", ""),
- });
- onOutput({
- type: "trace",
- message: output.message,
- });
+
+ if (inPanicHook && ndjsonType === "StdErr") {
+ // Check for stack backtrace header
+ if (output.message === "stack backtrace:") {
+ foundBacktraceHeader = true;
+ onOutput({
+ type: "trace",
+ message: "Stack trace (filtered):",
+ });
+ return;
+ }
+
+ if (foundBacktraceHeader) {
+ // Process stack trace lines
+ // Look for pattern: " N: ..." followed by " at ./file.rs:line"
+ if (STACK_FRAME_PATTERN.test(output.message)) {
+ traceLines.push(output.message);
+ } else if (LOCATION_PATTERN.test(output.message)) {
+ if (traceLines.length > 0) {
+ // Check if this is user code (not prog.rs)
+ if (!SYSTEM_CODE_PATTERN.test(output.message)) {
+ onOutput({
+ type: "trace",
+ message: traceLines[traceLines.length - 1].replace(
+ "prog::",
+ ""
+ ),
+ });
+ onOutput({
+ type: "trace",
+ message: output.message,
+ });
+ }
+ traceLines.pop(); // Remove the associated trace line (regardless of match)
}
- traceLines.pop(); // Remove the associated trace line (regardless of match)
}
+ return;
}
+
+ // Output panic messages as errors
+ onOutput({
+ type: "error",
+ message: output.message,
+ });
return;
}
-
- // Output panic messages as errors
- onOutput({
- type: "error",
- message: output.message,
- });
- return;
+
+ // Output normally
+ onOutput(output);
}
-
- // Output normally
- onOutput(output);
- });
+ );
}
diff --git a/app/terminal/worker/jsEval.ts b/app/terminal/worker/jsEval.ts
index 860c4e1..cf022d6 100644
--- a/app/terminal/worker/jsEval.ts
+++ b/app/terminal/worker/jsEval.ts
@@ -1,7 +1,7 @@
"use client";
import { createContext, useContext } from "react";
-import { RuntimeContext } from "../runtime";
+import { RuntimeContext, RuntimeInfo } from "../runtime";
import { ReplCommand, ReplOutput } from "../repl";
export const JSEvalContext = createContext(null!);
@@ -15,9 +15,14 @@ export function useJSEval() {
...context,
splitReplExamples,
getCommandlineStr,
+ runtimeInfo,
};
}
+const runtimeInfo: RuntimeInfo = {
+ prettyLangName: "JavaScript",
+};
+
function splitReplExamples(content: string): ReplCommand[] {
const initCommands: { command: string; output: ReplOutput[] }[] = [];
for (const line of content.split("\n")) {
diff --git a/app/terminal/worker/pyodide.ts b/app/terminal/worker/pyodide.ts
index eb616b7..e75a223 100644
--- a/app/terminal/worker/pyodide.ts
+++ b/app/terminal/worker/pyodide.ts
@@ -1,8 +1,9 @@
"use client";
import { createContext, useContext } from "react";
-import { RuntimeContext } from "../runtime";
+import { RuntimeContext, RuntimeInfo } from "../runtime";
import { ReplCommand, ReplOutput } from "../repl";
+import pyodideLock from "pyodide/pyodide-lock.json";
export const PyodideContext = createContext(null!);
@@ -15,9 +16,15 @@ export function usePyodide() {
...context,
splitReplExamples,
getCommandlineStr,
+ runtimeInfo,
};
}
+const runtimeInfo: RuntimeInfo = {
+ prettyLangName: "Python",
+ version: String(pyodideLock.info.python),
+};
+
function splitReplExamples(content: string): ReplCommand[] {
const initCommands: { command: string; output: ReplOutput[] }[] = [];
for (const line of content.split("\n")) {
diff --git a/app/terminal/worker/ruby.ts b/app/terminal/worker/ruby.ts
index e75b5cb..2fa79de 100644
--- a/app/terminal/worker/ruby.ts
+++ b/app/terminal/worker/ruby.ts
@@ -1,7 +1,7 @@
"use client";
import { createContext, useContext } from "react";
-import { RuntimeContext } from "../runtime";
+import { RuntimeContext, RuntimeInfo } from "../runtime";
import { ReplCommand, ReplOutput } from "../repl";
export const RubyContext = createContext(null!);
@@ -15,9 +15,15 @@ export function useRuby() {
...context,
splitReplExamples,
getCommandlineStr,
+ runtimeInfo,
};
}
+const runtimeInfo: RuntimeInfo = {
+ prettyLangName: "Ruby",
+ version: "3.4",
+};
+
function splitReplExamples(content: string): ReplCommand[] {
const initCommands: { command: string; output: ReplOutput[] }[] = [];
for (const line of content.split("\n")) {
diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts
index 121a2ed..5177187 100644
--- a/app/terminal/worker/ruby.worker.ts
+++ b/app/terminal/worker/ruby.worker.ts
@@ -50,6 +50,7 @@ async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{
try {
// Fetch and compile the Ruby WASM module
const rubyWasmRes = await fetch(
+ // ruby.ts 内にもRubyのバージョン(3.4)を直書きしている箇所がある
"https://cdn.jsdelivr.net/npm/@ruby/3.4-wasm-wasi@latest/dist/ruby+stdlib.wasm"
);
const rubyModule = await WebAssembly.compileStreaming(rubyWasmRes);