diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx index 4443e69..4cf98e5 100644 --- a/app/[docs_id]/markdown.tsx +++ b/app/[docs_id]/markdown.tsx @@ -158,10 +158,15 @@ function CodeComponent({ } else { // inline return ( - + {String(props.children || "").replace(/\n$/, "")} ); } } + +export function InlineCode({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index fdf2078..e280f3b 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -117,10 +117,14 @@ export function EditorComponent(props: EditorProps) { return (
-
- {props.filename} - {props.readonly && (編集不可)} -
+ + + {props.readonly + ? "出力されたファイル(編集不可):" + : "ファイルを編集:"} + + {props.filename} + - + {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);