Skip to content

Commit c80d9e2

Browse files
authored
Merge pull request #82 from ut-code/copilot/refactor-runtime-context-structure
Refactor terminal runtime contexts with unified RuntimeContext interface
2 parents b4a536f + 2a5e6d6 commit c80d9e2

File tree

16 files changed

+738
-542
lines changed

16 files changed

+738
-542
lines changed

app/[docs_id]/markdown.tsx

Lines changed: 18 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import Markdown, { Components } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import SyntaxHighlighter from "react-syntax-highlighter";
4-
import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
5-
import { type AceLang, EditorComponent } from "../terminal/editor";
6-
import { ExecFile, ExecLang } from "../terminal/exec";
4+
import { type AceLang, EditorComponent, getAceLang } from "../terminal/editor";
5+
import { ExecFile } from "../terminal/exec";
76
import { useChangeTheme } from "./themeToggle";
87
import {
98
tomorrow,
109
atomOneDark,
1110
} from "react-syntax-highlighter/dist/esm/styles/hljs";
1211
import { ReactNode } from "react";
12+
import { getRuntimeLang, RuntimeLang } from "@/terminal/runtime";
13+
import { ReplTerminal } from "@/terminal/repl";
1314

1415
export function StyledMarkdown({ content }: { content: string }) {
1516
return (
@@ -94,6 +95,7 @@ function CodeComponent({
9495
className || ""
9596
);
9697
if (match) {
98+
const runtimeLang = getRuntimeLang(match[1]);
9799
if (match[2] === "-exec" && match[3]) {
98100
/*
99101
```python-exec:main.py
@@ -105,24 +107,11 @@ function CodeComponent({
105107
hello, world!
106108
---------------------------
107109
*/
108-
let execLang: ExecLang | undefined = undefined;
109-
switch (match[1]) {
110-
case "python":
111-
execLang = "python";
112-
break;
113-
case "cpp":
114-
case "c++":
115-
execLang = "cpp";
116-
break;
117-
default:
118-
console.warn(`Unsupported language for exec: ${match[1]}`);
119-
break;
120-
}
121-
if (execLang) {
110+
if (runtimeLang) {
122111
return (
123112
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
124113
<ExecFile
125-
language={execLang}
114+
language={runtimeLang}
126115
filenames={match[3].split(",")}
127116
content={String(props.children || "").replace(/\n$/, "")}
128117
/>
@@ -131,56 +120,29 @@ function CodeComponent({
131120
}
132121
} else if (match[2] === "-repl") {
133122
// repl付きの言語指定
134-
// 現状はPythonのみ対応
135123
if (!match[3]) {
136124
console.warn(
137125
`${match[1]}-repl without terminal id! content: ${String(props.children).slice(0, 20)}...`
138126
);
139127
}
140-
switch (match[1]) {
141-
case "python":
142-
return (
143-
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
144-
<PythonEmbeddedTerminal
145-
terminalId={match[3]}
146-
content={String(props.children || "").replace(/\n$/, "")}
147-
/>
148-
</div>
149-
);
150-
default:
151-
console.warn(`Unsupported language for repl: ${match[1]}`);
152-
break;
128+
if (runtimeLang) {
129+
return (
130+
<div className="bg-base-300 border border-primary border-2 shadow-md m-2 p-4 pr-1 rounded-lg">
131+
<ReplTerminal
132+
terminalId={match[3]}
133+
language={runtimeLang}
134+
initContent={String(props.children || "").replace(/\n$/, "")}
135+
/>
136+
</div>
137+
);
153138
}
154139
} else if (match[3]) {
155140
// ファイル名指定がある場合、ファイルエディター
156-
let aceLang: AceLang | undefined = undefined;
157-
switch (match[1]) {
158-
case "python":
159-
aceLang = "python";
160-
break;
161-
case "cpp":
162-
case "c++":
163-
aceLang = "c_cpp";
164-
break;
165-
case "json":
166-
aceLang = "json";
167-
break;
168-
case "csv":
169-
aceLang = "csv";
170-
break;
171-
case "text":
172-
case "txt":
173-
aceLang = "text";
174-
break;
175-
default:
176-
console.warn(`Unsupported language for editor: ${match[1]}`);
177-
break;
178-
}
141+
const aceLang = getAceLang(match[1]);
179142
return (
180143
<div className="border border-primary border-2 shadow-md m-2 rounded-lg">
181144
<EditorComponent
182145
language={aceLang}
183-
tabSize={4}
184146
filename={match[3]}
185147
readonly={match[2] === "-readonly"}
186148
initContent={String(props.children || "").replace(/\n$/, "")}

app/layout.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import "./globals.css";
55
import { Navbar } from "./navbar";
66
import { Sidebar } from "./sidebar";
77
import { ReactNode } from "react";
8-
import { PyodideProvider } from "./terminal/python/pyodide";
9-
import { WandboxProvider } from "./terminal/wandbox/wandbox";
108
import { EmbedContextProvider } from "./terminal/embedContext";
119
import { AutoAnonymousLogin } from "./accountMenu";
1210
import { SidebarMdProvider } from "./[docs_id]/dynamicMdContext";
11+
import { RuntimeProvider } from "./terminal/runtime";
1312

1413
export const metadata: Metadata = {
1514
title: "Create Next App",
@@ -25,13 +24,15 @@ export default function RootLayout({
2524
<AutoAnonymousLogin />
2625
<SidebarMdProvider>
2726
<div className="drawer lg:drawer-open">
28-
<input id="drawer-toggle" type="checkbox" className="drawer-toggle" />
27+
<input
28+
id="drawer-toggle"
29+
type="checkbox"
30+
className="drawer-toggle"
31+
/>
2932
<div className="drawer-content flex flex-col">
3033
<Navbar />
3134
<EmbedContextProvider>
32-
<PyodideProvider>
33-
<WandboxProvider>{children}</WandboxProvider>
34-
</PyodideProvider>
35+
<RuntimeProvider>{children}</RuntimeProvider>
3536
</EmbedContextProvider>
3637
</div>
3738
<div className="drawer-side shadow-md z-50">

app/terminal/README.md

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

app/terminal/editor.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,37 @@ import { useEffect } from "react";
2424
import clsx from "clsx";
2525
import { useChangeTheme } from "../[docs_id]/themeToggle";
2626
import { useEmbedContext } from "./embedContext";
27+
import { langConstants } from "./runtime";
2728
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
2829

2930
// mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する
3031
export type AceLang = "python" | "c_cpp" | "json" | "csv" | "text";
32+
export function getAceLang(lang: string | undefined): AceLang {
33+
// Markdownで指定される可能性のある言語名からAceLangを取得
34+
switch (lang) {
35+
case "python":
36+
case "py":
37+
return "python";
38+
case "cpp":
39+
case "c++":
40+
return "c_cpp";
41+
case "json":
42+
return "json";
43+
case "csv":
44+
return "csv";
45+
case "text":
46+
case "txt":
47+
return "text";
48+
default:
49+
console.warn(
50+
`Unsupported language for ace editor: ${lang}, fallback to text mode.`
51+
);
52+
return "text";
53+
}
54+
}
3155

3256
interface EditorProps {
3357
language?: AceLang;
34-
tabSize: number;
3558
filename: string;
3659
initContent: string;
3760
readonly?: boolean;
@@ -88,7 +111,7 @@ export function EditorComponent(props: EditorProps) {
88111
name={`ace-editor-${props.filename}`}
89112
mode={props.language}
90113
theme={theme}
91-
tabSize={props.tabSize}
114+
tabSize={langConstants(props.language || "text").tabSize}
92115
width="100%"
93116
height={
94117
Math.max((props.initContent.split("\n").length + 2) * 14, 128) + "px"

0 commit comments

Comments
 (0)