Skip to content

Commit f9e0fe7

Browse files
committed
runtimeリファクタ&README追加
1 parent 426df51 commit f9e0fe7

File tree

8 files changed

+339
-230
lines changed

8 files changed

+339
-230
lines changed

app/terminal/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# my.code(); Runtime API
2+
3+
## runtime.ts
4+
5+
各言語のランタイムはRuntimeContextインターフェースの実装を返すフックを実装する必要があります。
6+
7+
runtime.ts の `useRuntime(lang)` は各言語のフックを呼び出し、その中で指定された言語のランタイムを返します。
8+
9+
関数はすべてuseCallbackやuseMemoなどを用いレンダリングごとに同じインスタンスを返すように実装してください。
10+
11+
### 共通
12+
13+
* ready: `boolean`
14+
* ランタイムの初期化が完了したか、不要である場合true
15+
* tabSize: `number`
16+
* REPLおよびコードエディターののタブ幅を指定します。
17+
* mutex?: `MutexInterface`
18+
* ランタイムに排他制御が必要な場合、MutexInterfaceのインスタンスを返してください。
19+
* interrupt?: `() => Promise<void>`
20+
* 実行中のコマンドを中断します。呼び出し側でmutexのロックはされません
21+
22+
### REPL用
23+
24+
* runCommand?: `(command: string) => Promise<ReplOutput[]>`
25+
* コマンドを実行します。実行結果をReplOutputの配列で返します。
26+
* runCommandを呼び出す際には呼び出し側 (主に repl.tsx) でmutexをロックします。複数のコマンドを連続実行したい場合があるからです。
27+
* checkSyntax?: `(code: string) => Promise<SyntaxStatus>`
28+
* コードの構文チェックを行います。行がコマンドとして完結していれば`complete`、次の行に続く場合(if文の条件式の途中など)は`incomplete`を返してください。
29+
* REPLでEnterを押した際の動作に影響します。
30+
* 呼び出し側でmutexのロックはされません
31+
* splitReplExamples?: `(code: string) => ReplCommands[]`
32+
* markdown内に記述されているREPLのサンプルコードをパースします。例えば
33+
```
34+
>>> if True:
35+
... print("Hello")
36+
Hello
37+
```
38+
をsplitReplExamplesに通すと
39+
```ts
40+
[
41+
{
42+
command: 'if True:\n print("Hello")',
43+
output: {
44+
type: 'output',
45+
content: 'Hello'
46+
}
47+
}
48+
]
49+
```
50+
が返されるようにします。
51+
* prompt?: `string`
52+
* REPLの1行目のプロンプト文字列を指定します。
53+
* promptMore?: `string`
54+
* REPLの2行目以降のプロンプト文字列を指定します。
55+
56+
### ファイル実行用
57+
58+
* runFiles: `(filenames: string[]) => Promise<ReplOutput[]>`
59+
* 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。
60+
* 呼び出し側でmutexのロックはされません
61+
* getCommandlineStr: `(filenames: string[]) => string`
62+
* 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。
63+
64+
## embedContext.tsx
65+
66+
Replの実行結果(`replOutputs`)、ユーザーが編集したファイル(`files`)、ファイルの実行結果(`execResults`)の情報を保持します。
67+
68+
## terminal.tsx
69+
70+
xterm.jsを制御する useTerminal() フックを提供します。
71+
リサイズやテーマ切り替えなどの処理を行います。
72+
73+
引数:
74+
* getRows?: `(cols: number) => number`
75+
* ターミナルの幅がcolsの場合の高さの最小値を指定します。
76+
* 未指定または5未満の場合5になります。
77+
* 内部でuseRefを使用しターミナル初期化完了の瞬間のgetRows関数インスタンスが呼び出されるので、一時オブジェクトでも大丈夫
78+
* onReady?: `() => void`
79+
* ターミナルが初期化された際に呼び出されます。
80+
* 内部でuseRefを使用しターミナル初期化完了の瞬間のonReady関数インスタンスが呼び出されるので、一時オブジェクトでも大丈夫
81+
82+
返り値:
83+
* terminalRef: `RefObject<HTMLDivElement>`
84+
* ターミナルを描画するためのdiv要素にこのrefを渡してください。
85+
* terminalInstanceRef: `RefObject<Terminal | null>`
86+
* xterm.jsのTerminalインスタンスへのrefです。
87+
* termReady: `boolean`
88+
* ターミナルが初期化されたかどうかを示します。
89+
90+
## repl.tsx
91+
92+
ReplTerminal コンポーネントを提供します。
93+
useRuntimeとuseTerminalを呼び出し、REPLの入出力、キーハンドリング処理を行います。
94+
95+
また、実行したコマンド結果はEmbedContextに送信されます。
96+
97+
シンタックスハイライトはprism.jsでパースしたものを独自処理で色付けしています。(highlight.ts 内の highlightCodeToAnsi 関数)
98+
パース処理の実装は不要ですがhighlight.tsに言語定義のインポートとswitch文分岐の追加が必要です。
99+
100+
## editor.tsx
101+
102+
EditorComponent コンポーネントを提供します。
103+
104+
ファイルの内容はEmbedContextと同期されます。
105+
106+
## exec.tsx
107+
108+
実行ボタンと結果を表示する ExecFile コンポーネントを提供します。
109+
110+
実行結果はEmbedContextに送信されます。
111+
112+
## 各言語の実装
113+
114+
### Pyodide (Python)
115+
116+
Pyodide を web worker で動かしています。
117+
ランタイムの初期化に時間がかかるため、バックグラウンドで初期化を行います。
118+

app/terminal/exec.tsx

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,17 @@ import {
88
useTerminal,
99
} from "./terminal";
1010
import { writeOutput } from "./repl";
11-
import { useState } from "react";
11+
import { useEffect, useState } from "react";
1212
import { useEmbedContext } from "./embedContext";
13-
import { useRuntime } from "./runtime";
14-
15-
export type ExecLang = "python" | "cpp";
13+
import { RuntimeLang, useRuntime } from "./runtime";
1614

1715
interface ExecProps {
1816
/*
1917
* Pythonの場合はメインファイル1つのみを指定する。
2018
* C++の場合はソースコード(.cpp)を全部指定する。
2119
*/
2220
filenames: string[];
23-
language: ExecLang;
21+
language: RuntimeLang;
2422
content: string;
2523
}
2624
export function ExecFile(props: ExecProps) {
@@ -33,70 +31,65 @@ export function ExecFile(props: ExecProps) {
3331
}
3432
},
3533
});
36-
const sectionContext = useEmbedContext();
34+
const { setExecResult } = useEmbedContext();
3735

38-
const runtime = useRuntime(props.language);
36+
const { ready, runFiles, getCommandlineStr } = useRuntime(props.language);
3937

40-
// 表示するコマンドライン文字列
41-
let commandline: string;
42-
if (props.language === "python") {
43-
if (props.filenames.length !== 1) {
44-
throw new Error("Pythonの実行にはファイル名が1つ必要です");
45-
}
46-
commandline = `python ${props.filenames[0]}`;
47-
} else if (props.language === "cpp") {
48-
if (!props.filenames || props.filenames.length === 0) {
49-
throw new Error("C++の実行には filenames プロパティが必要です");
38+
// ユーザーがクリックした時(triggered) && ランタイムが準備できた時に、実際にinitCommandを実行する(executing)
39+
const [executionState, setExecutionState] = useState<
40+
"idle" | "triggered" | "executing"
41+
>("idle");
42+
useEffect(() => {
43+
if (executionState === "triggered" && ready) {
44+
setExecutionState("executing");
45+
(async () => {
46+
clearTerminal(terminalInstanceRef.current!);
47+
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
48+
const outputs = await runFiles(props.filenames);
49+
clearTerminal(terminalInstanceRef.current!);
50+
writeOutput(terminalInstanceRef.current!, outputs, false);
51+
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
52+
setExecResult(props.filenames.join(","), outputs);
53+
setExecutionState("idle");
54+
})();
5055
}
51-
commandline = runtime.getCommandlineStr
52-
? runtime.getCommandlineStr(props.filenames)
53-
: `g++ ${props.filenames.join(" ")}`;
54-
} else {
55-
props.language satisfies never;
56-
commandline = `エラー: 非対応の言語 ${props.language}`;
57-
}
58-
59-
const runtimeInitializing = runtime.initializing;
60-
const beforeExec = runtime.ready ? null : runtime.init;
61-
const exec = () => runtime.runFiles(props.filenames);
56+
}, [
57+
executionState,
58+
ready,
59+
props.filenames,
60+
runFiles,
61+
setExecResult,
62+
terminalInstanceRef,
63+
]);
6264

63-
// 実行中です... と表示される
64-
const [isExecuting, setIsExecuting] = useState<boolean>(false);
65-
66-
const onClick = async () => {
67-
if (beforeExec) {
68-
clearTerminal(terminalInstanceRef.current!);
69-
terminalInstanceRef.current!.write(
70-
systemMessageColor("(初期化しています...しばらくお待ちください)")
71-
);
72-
await beforeExec();
73-
}
74-
clearTerminal(terminalInstanceRef.current!);
75-
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
76-
setIsExecuting(true);
77-
const outputs = await exec();
78-
setIsExecuting(false);
79-
clearTerminal(terminalInstanceRef.current!);
80-
writeOutput(terminalInstanceRef.current!, outputs, false);
81-
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
82-
sectionContext?.setExecResult(props.filenames.join(","), outputs);
83-
};
8465
return (
8566
<div className="relative">
8667
<div>
8768
<button
8869
className="btn btn-soft btn-primary rounded-tl-lg rounded-none"
89-
onClick={onClick}
90-
disabled={!termReady || runtimeInitializing}
70+
onClick={() => {
71+
if (!ready) {
72+
clearTerminal(terminalInstanceRef.current!);
73+
terminalInstanceRef.current!.write(
74+
systemMessageColor(
75+
"(初期化しています...しばらくお待ちください)"
76+
)
77+
);
78+
}
79+
setExecutionState("triggered");
80+
}}
81+
disabled={!termReady || executionState !== "idle"}
9182
>
9283
▶ 実行
9384
</button>
94-
<code className="text-sm ml-4">{commandline}</code>
85+
<code className="text-sm ml-4">
86+
{getCommandlineStr(props.filenames)}
87+
</code>
9588
</div>
9689
<div className="bg-base-300 p-4 pt-2 rounded-b-lg">
9790
<div ref={terminalRef} />
9891
</div>
99-
{(runtimeInitializing || isExecuting) && (
92+
{executionState !== "idle" && (
10093
<div className="absolute z-10 inset-0 cursor-wait" />
10194
)}
10295
</div>

app/terminal/highlight.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
import Prism from "prismjs";
22
import chalk from "chalk";
3+
import { RuntimeLang } from "./runtime";
34
// Python言語定義をインポート
45
import "prismjs/components/prism-python";
56

7+
type PrismLang = "python";
8+
9+
function getPrismLanguage(language: RuntimeLang): PrismLang {
10+
switch (language) {
11+
case "python":
12+
return "python";
13+
case "cpp":
14+
throw new Error(
15+
`highlight for ${language} is disabled because it should not support REPL`
16+
);
17+
default:
18+
language satisfies never;
19+
throw new Error(`Prism language not implemented for: ${language}`);
20+
}
21+
}
22+
623
const nothing = (text: string): string => text;
724

825
// PrismのトークンクラスとANSIコードをマッピング
@@ -50,12 +67,15 @@ const prismToAnsi: Record<string, (text: string) => string> = {
5067
* @param {string} code ハイライト対象のPythonコード
5168
* @returns {string} ANSIで色付けされた文字列
5269
*/
53-
export function highlightCodeToAnsi(code: string, language: string): string {
70+
export function highlightCodeToAnsi(
71+
code: string,
72+
language: RuntimeLang
73+
): string {
5474
// Prismでハイライト処理を行い、HTML文字列を取得
5575
const highlightedHtml = Prism.highlight(
5676
code,
57-
Prism.languages[language],
58-
language
77+
Prism.languages[getPrismLanguage(language)],
78+
getPrismLanguage(language)
5979
);
6080

6181
// 一時的なDOM要素を作成してパース

app/terminal/python/embedded.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.

app/terminal/python/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default function PythonPage() {
1010
<ReplTerminal
1111
terminalId=""
1212
language="python"
13-
initMessage="Welcome to Pyodide Terminal!"
13+
initContent={">>> print('hello, world!')\nhello, world!"}
1414
/>
1515
<EditorComponent
1616
language="python"

0 commit comments

Comments
 (0)