Skip to content

Commit c2dc3e6

Browse files
authored
Merge pull request #163 from ut-code/repl-help
実行環境のヘルプ表示と停止ボタンを追加
2 parents 85ab8df + 0382f3a commit c2dc3e6

File tree

14 files changed

+306
-132
lines changed

14 files changed

+306
-132
lines changed

app/[docs_id]/markdown.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,15 @@ function CodeComponent({
158158
} else {
159159
// inline
160160
return (
161-
<code
162-
className="bg-current/10 border border-current/20 px-1 py-0.5 mx-0.5 rounded-md"
163-
{...props}
164-
/>
161+
<InlineCode>{String(props.children || "").replace(/\n$/, "")}</InlineCode>
165162
);
166163
}
167164
}
165+
166+
export function InlineCode({ children }: { children: ReactNode }) {
167+
return (
168+
<code className="bg-current/10 border border-current/20 px-1 py-0.5 mx-0.5 rounded-md">
169+
{children}
170+
</code>
171+
);
172+
}

app/terminal/editor.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,14 @@ export function EditorComponent(props: EditorProps) {
117117
return (
118118
<div className="border border-accent border-2 shadow-md m-2 rounded-box overflow-hidden">
119119
<div className="flex flex-row items-center bg-base-200">
120-
<div className="font-mono text-sm mt-2 mb-1 ml-4 mr-2">
121-
{props.filename}
122-
{props.readonly && <span className="font-sans ml-2">(編集不可)</span>}
123-
</div>
120+
<span className="mt-2 mb-1 ml-3 mr-2 text-sm text-left">
121+
<span>
122+
{props.readonly
123+
? "出力されたファイル(編集不可):"
124+
: "ファイルを編集:"}
125+
</span>
126+
<span className="font-mono ml-2">{props.filename}</span>
127+
</span>
124128
<button
125129
className={clsx(
126130
"btn btn-xs btn-soft btn-warning mt-1 mb-1",

app/terminal/exec.tsx

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export function ExecFile(props: ExecProps) {
3434
});
3535
const { files, clearExecResult, addExecOutput } = useEmbedContext();
3636

37-
const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);
37+
const { ready, runFiles, getCommandlineStr, runtimeInfo, interrupt } =
38+
useRuntime(props.language);
3839

3940
// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing)
4041
const [executionState, setExecutionState] = useState<
@@ -82,13 +83,16 @@ export function ExecFile(props: ExecProps) {
8283
]);
8384

8485
return (
85-
<div className="border border-accent border-2 shadow-md m-2 rounded-box overflow-hidden relative">
86-
<div className="bg-base-200 flex items-center">
86+
<div className="border border-accent border-2 shadow-md m-2 rounded-box relative">
87+
<div className="bg-base-200 flex items-center rounded-t-box">
8788
<button
8889
/* daisyuiのbtnはheightがvar(--size)で固定。
8990
ここでは最小でそのサイズ、ただし親コンテナがそれより大きい場合に大きくしたい
9091
→ heightを解除し、min-heightをデフォルトのサイズと同じにする */
91-
className="btn btn-soft btn-accent rounded-none h-[unset]! min-h-(--size) self-stretch"
92+
className={clsx(
93+
"btn btn-soft btn-accent h-[unset]! min-h-(--size) self-stretch",
94+
"rounded-none rounded-tl-[calc(var(--radius-box)-2px)]"
95+
)}
9296
onClick={() => {
9397
if (!ready) {
9498
clearTerminal(terminalInstanceRef.current!);
@@ -98,17 +102,58 @@ export function ExecFile(props: ExecProps) {
98102
)
99103
);
100104
}
101-
setExecutionState("triggered");
105+
if (executionState === "idle") {
106+
setExecutionState("triggered");
107+
}
108+
if (executionState === "executing" && interrupt) {
109+
// Ctrl+C
110+
interrupt();
111+
terminalInstanceRef.current!.write("^C");
112+
}
102113
}}
103-
disabled={!termReady || executionState !== "idle"}
114+
disabled={
115+
!termReady ||
116+
!(
117+
executionState === "idle" ||
118+
(executionState === "executing" && interrupt)
119+
)
120+
}
104121
>
105-
▶ 実行
122+
{executionState === "idle" ? "▶ 実行" : "■ 停止"}
106123
</button>
107-
<code className="text-left break-all text-sm my-1 ml-4 mr-1">
124+
<code className="text-left break-all text-sm my-1 ml-4">
108125
{getCommandlineStr?.(props.filenames)}
109126
</code>
127+
<div className="ml-1 mr-1 tooltip tooltip-secondary tooltip-bottom z-1">
128+
{/*なぜかわからないがz-1がないと後ろに隠れてしまう*/}
129+
<div className="tooltip-content bg-secondary/60 backdrop-blur-xs">
130+
ブラウザ上で動作する
131+
<span className="mx-0.5">
132+
{runtimeInfo?.prettyLangName || props.language}
133+
</span>
134+
{runtimeInfo?.version && (
135+
<span className="mr-0.5">{runtimeInfo?.version}</span>
136+
)}
137+
の実行環境です。
138+
<br />
139+
左上の実行ボタンを押して、このページ内の
140+
{props.filenames.map((fname) => (
141+
<span key={fname}>
142+
<span className="font-mono mx-0.5">{fname}</span>
143+
</span>
144+
))}
145+
に書かれている内容を実行します。
146+
</div>
147+
<button
148+
className={clsx(
149+
"btn btn-xs btn-soft btn-secondary rounded-full cursor-help"
150+
)}
151+
>
152+
153+
</button>
154+
</div>
110155
</div>
111-
<div className="bg-base-300 p-4 pt-2 relative">
156+
<div className="bg-base-300 p-4 pr-1 pt-2 relative rounded-b-box">
112157
{/*
113158
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
114159
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
@@ -130,10 +175,10 @@ export function ExecFile(props: ExecProps) {
130175
)}
131176
ref={terminalRef}
132177
/>
178+
{executionState !== "idle" && (
179+
<div className="absolute z-10 inset-0 cursor-wait" />
180+
)}
133181
</div>
134-
{executionState !== "idle" && (
135-
<div className="absolute z-10 inset-0 cursor-wait" />
136-
)}
137182
</div>
138183
);
139184
}

app/terminal/repl.tsx

Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { Terminal } from "@xterm/xterm";
1616
import { useEmbedContext } from "./embedContext";
1717
import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime";
1818
import clsx from "clsx";
19+
import { InlineCode } from "@/[docs_id]/markdown";
1920

2021
export type ReplOutputType =
2122
| "stdout"
@@ -97,6 +98,7 @@ export function ReplTerminal({
9798
runCommand,
9899
checkSyntax,
99100
splitReplExamples,
101+
runtimeInfo,
100102
} = useRuntime(language);
101103
const { tabSize, prompt, promptMore, returnPrefix } = langConstants(language);
102104
if (!prompt) {
@@ -129,6 +131,10 @@ export function ReplTerminal({
129131
// REPLのユーザー入力
130132
const inputBuffer = useRef<string[]>([]);
131133

134+
const [executionState, setExecutionState] = useState<"idle" | "executing">(
135+
"idle"
136+
);
137+
132138
// inputBufferを更新し、画面に描画する
133139
const updateBuffer = useCallback(
134140
(newBuffer: (() => string[]) | null, insertBefore?: () => void) => {
@@ -219,6 +225,7 @@ export function ReplTerminal({
219225
const command = inputBuffer.current.join("\n").trim();
220226
inputBuffer.current = [];
221227
const commandId = addReplCommand(terminalId, command);
228+
setExecutionState("executing");
222229
let executionDone = false;
223230
await runtimeMutex.runExclusive(async () => {
224231
await runCommand(command, (output) => {
@@ -233,6 +240,7 @@ export function ReplTerminal({
233240
addReplOutput(terminalId, commandId, output);
234241
});
235242
});
243+
setExecutionState("idle");
236244
executionDone = true;
237245
updateBuffer(() => [""]);
238246
}
@@ -378,51 +386,109 @@ export function ReplTerminal({
378386
]);
379387

380388
return (
381-
<div className="bg-base-300 border border-accent border-2 shadow-md m-2 p-4 pr-1 rounded-box relative h-max">
389+
<div className="bg-base-300 border border-accent border-2 shadow-md m-2 rounded-box h-max">
390+
<div className="bg-base-200 flex items-center rounded-t-box">
391+
<button
392+
/* daisyuiのbtnはheightがvar(--size)で固定。
393+
ここでは最小でそのサイズ、ただし親コンテナがそれより大きい場合に大きくしたい
394+
→ heightを解除し、min-heightをデフォルトのサイズと同じにする */
395+
className={clsx(
396+
"btn btn-soft btn-accent h-[unset]! min-h-(--size) self-stretch",
397+
"rounded-none rounded-tl-[calc(var(--radius-box)-2px)]"
398+
)}
399+
onClick={() => {
400+
// Ctrl+C
401+
if (terminalInstanceRef.current && runtimeInterrupt) {
402+
runtimeInterrupt();
403+
terminalInstanceRef.current.write("^C");
404+
}
405+
}}
406+
disabled={
407+
!termReady ||
408+
initCommandState !== "done" ||
409+
executionState !== "executing"
410+
}
411+
>
412+
■ 停止
413+
</button>
414+
<span className="text-sm my-1 ml-3 text-left">
415+
{runtimeInfo?.prettyLangName || language} 実行環境
416+
</span>
417+
<div className="ml-1 tooltip tooltip-secondary tooltip-bottom">
418+
<div className="tooltip-content bg-secondary/60 backdrop-blur-xs">
419+
ブラウザ上で動作する
420+
<span className="mx-0.5">
421+
{runtimeInfo?.prettyLangName || language}
422+
</span>
423+
{runtimeInfo?.version && (
424+
<span className="mr-0.5">{runtimeInfo?.version}</span>
425+
)}
426+
のREPL実行環境です。
427+
<br />
428+
プロンプト (<InlineCode>{prompt?.trimEnd()}</InlineCode>)
429+
の後にコマンドを入力し、
430+
<kbd className="kbd kbd-sm text-base-content">Enter</kbd>
431+
キーで実行します。
432+
<br />
433+
<kbd className="kbd kbd-sm text-base-content">Ctrl</kbd>+
434+
<kbd className="kbd kbd-sm text-base-content">C</kbd>
435+
または左上の停止ボタンで実行中のコマンドを中断できます。
436+
</div>
437+
<button
438+
className={clsx(
439+
"btn btn-xs btn-soft btn-secondary rounded-full cursor-help"
440+
)}
441+
>
442+
443+
</button>
444+
</div>
445+
</div>
382446
{/*
383447
ターミナル表示の初期化が完了するまでの間、ターミナルは隠し、内容をそのまま表示する。
384448
可能な限りレイアウトが崩れないようにするため & SSRでも内容が読めるように(SEO?)という意味もある
385449
*/}
386-
<pre
387-
className={clsx(
388-
"font-mono overflow-auto cursor-wait",
389-
"min-h-26", // xterm.jsで5行分の高さ
390-
initCommandState !== "initializing" && "hidden"
391-
)}
392-
>
393-
{initContent + "\n\n"}
394-
</pre>
395-
{terminalInstanceRef.current &&
396-
termReady &&
397-
initCommandState === "idle" && (
398-
<div
399-
className="absolute z-10 inset-0 cursor-pointer"
400-
onClick={() => {
401-
if (!runtimeReady) {
402-
hideCursor(terminalInstanceRef.current!);
403-
terminalInstanceRef.current!.write(
404-
systemMessageColor(
405-
"(初期化しています...しばらくお待ちください)"
406-
)
407-
);
408-
terminalInstanceRef.current!.focus();
409-
}
410-
setInitCommandState("triggered");
411-
}}
412-
/>
413-
)}
414-
{(initCommandState === "triggered" ||
415-
initCommandState === "executing") && (
416-
<div className="absolute z-10 inset-0 cursor-wait" />
417-
)}
418-
<div
419-
className={clsx(
420-
initCommandState === "initializing" &&
421-
/* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/
422-
"absolute invisible"
450+
<div className="relative p-4 pr-1 pt-2">
451+
<pre
452+
className={clsx(
453+
"font-mono overflow-auto cursor-wait",
454+
"min-h-26", // xterm.jsで5行分の高さ
455+
initCommandState !== "initializing" && "hidden"
456+
)}
457+
>
458+
{initContent + "\n\n"}
459+
</pre>
460+
{terminalInstanceRef.current &&
461+
termReady &&
462+
initCommandState === "idle" && (
463+
<div
464+
className="absolute z-10 inset-0 cursor-pointer"
465+
onClick={() => {
466+
if (!runtimeReady) {
467+
hideCursor(terminalInstanceRef.current!);
468+
terminalInstanceRef.current!.write(
469+
systemMessageColor(
470+
"(初期化しています...しばらくお待ちください)"
471+
)
472+
);
473+
terminalInstanceRef.current!.focus();
474+
}
475+
setInitCommandState("triggered");
476+
}}
477+
/>
478+
)}
479+
{(initCommandState === "triggered" ||
480+
initCommandState === "executing") && (
481+
<div className="absolute z-10 inset-0 cursor-wait" />
423482
)}
424-
ref={terminalRef}
425-
/>
483+
<div
484+
className={clsx(
485+
initCommandState === "initializing" &&
486+
/* "hidden" だとterminalがdivのサイズを取得しようとしたときにバグる*/
487+
"absolute invisible"
488+
)}
489+
ref={terminalRef}
490+
/>
491+
</div>
426492
</div>
427493
);
428494
}

app/terminal/runtime.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export interface RuntimeContext {
3737
onOutput: (output: ReplOutput) => void
3838
) => Promise<void>;
3939
getCommandlineStr?: (filenames: string[]) => string;
40+
runtimeInfo?: RuntimeInfo;
41+
}
42+
export interface RuntimeInfo {
43+
prettyLangName: string;
44+
version?: string;
4045
}
4146
export interface LangConstants {
4247
tabSize: number;

0 commit comments

Comments
 (0)