Skip to content

Commit c5e812e

Browse files
committed
ファイルエディターの実装
1 parent 92a6bf0 commit c5e812e

10 files changed

Lines changed: 236 additions & 54 deletions

File tree

app/[docs_id]/markdown.tsx

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import Markdown, { Components } from "react-markdown";
22
import remarkGfm from "remark-gfm";
33
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4-
import { PythonEmbeddedTerminal } from "../terminal/python/embedded";
4+
import { PythonEmbeddedTerminal, PythonExecFile } from "../terminal/python/embedded";
55
import { Heading } from "./section";
6+
import { EditorComponent } from "../terminal/editor";
67

78
export function StyledMarkdown({ content }: { content: string }) {
89
return (
@@ -32,52 +33,86 @@ const components: Components = {
3233
strong: ({ node, ...props }) => (
3334
<strong className="text-primary" {...props} />
3435
),
36+
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
37+
pre: ({ node, ...props }) => props.children,
3538
code: ({ node, className, ref, style, ...props }) => {
36-
const match = /^language-(\w+)(-repl)?\:?(.+)?$/.exec(className || "");
39+
const match = /^language-(\w+)(-repl|-exec)?\:?(.+)?$/.exec(className || "");
3740
if (match) {
38-
if (match[2]) {
39-
// repl付きの言語指定
41+
if(match[2] === "-exec" && match[3]) {
42+
/*
43+
```python-exec:main.py
44+
hello, world!
45+
```
46+
47+
---------------------------
48+
[▶ 実行] `python main.py`
49+
hello, world!
50+
---------------------------
51+
*/
52+
switch(match[1]){
53+
case "python":
54+
return (
55+
<div className="border border-primary m-2 rounded-lg">
56+
<PythonExecFile filename={match[3]} content={String(props.children).replace(/\n$/, "")} />
57+
</div>
58+
);
59+
default:
60+
console.warn(`Unsupported language for exec: ${match[1]}`);
61+
break;
62+
}
63+
}else if (match[3]) {
64+
// ファイル名指定がある場合、ファイルエディター
4065
// 現状はPythonのみ対応
4166
switch (match[1]) {
4267
case "python":
4368
return (
44-
<PythonEmbeddedTerminal
45-
content={String(props.children).replace(/\n$/, "")}
46-
/>
69+
<div className="border border-primary m-2 rounded-lg">
70+
<EditorComponent
71+
language={match[1]}
72+
tabSize={4}
73+
filename={match[3]}
74+
initContent={String(props.children).replace(/\n$/, "")}
75+
/>
76+
</div>
4777
);
4878
default:
49-
console.warn(`Unsupported language for repl: ${match[1]}`);
79+
console.warn(`Unsupported language for editor: ${match[1]}`);
80+
break;
81+
}
82+
}else if (match[2] === "-repl") {
83+
// repl付きの言語指定
84+
// 現状はPythonのみ対応
85+
switch (match[1]) {
86+
case "python":
5087
return (
51-
<SyntaxHighlighter
52-
language={match[1]}
53-
PreTag="div"
54-
className="px-4! py-4! m-0! font-mono!"
55-
// style={todo dark theme?}
56-
{...props}
57-
>
58-
{String(props.children).replace(/\n$/, "")}
59-
</SyntaxHighlighter>
88+
<div className="bg-base-300 border border-primary m-2 p-4 rounded-lg">
89+
<PythonEmbeddedTerminal
90+
content={String(props.children).replace(/\n$/, "")}
91+
/>
92+
</div>
6093
);
94+
default:
95+
console.warn(`Unsupported language for repl: ${match[1]}`);
96+
break;
6197
}
62-
} else {
63-
return (
64-
<SyntaxHighlighter
65-
language={match[1]}
66-
PreTag="div"
67-
className="px-4! py-4! m-0! font-mono!"
68-
// style={todo dark theme?}
69-
{...props}
70-
>
71-
{String(props.children).replace(/\n$/, "")}
72-
</SyntaxHighlighter>
73-
);
7498
}
99+
return (
100+
<SyntaxHighlighter
101+
language={match[1]}
102+
PreTag="div"
103+
className="border border-primary mx-2 my-2 rounded-lg text-sm! m-2! p-4! font-mono!"
104+
// style={todo dark theme?}
105+
{...props}
106+
>
107+
{String(props.children).replace(/\n$/, "")}
108+
</SyntaxHighlighter>
109+
);
75110
} else if (String(props.children).includes("\n")) {
76111
// 言語指定なしコードブロック
77112
return (
78113
<SyntaxHighlighter
79114
PreTag="div"
80-
className="px-4! py-4! m-0! font-mono!"
115+
className="border border-primary mx-2 my-2 rounded-lg text-sm! m-2! p-4! font-mono!"
81116
// style={todo dark theme?}
82117
{...props}
83118
>
@@ -94,11 +129,4 @@ const components: Components = {
94129
);
95130
}
96131
},
97-
pre: ({ node, ...props }) => (
98-
<pre
99-
className="bg-base-200 border border-primary mx-2 my-2 rounded-lg text-sm overflow-x-auto"
100-
{...props}
101-
/>
102-
),
103-
hr: ({ node, ...props }) => <hr className="border-primary my-4" {...props} />,
104132
};

app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Navbar } from "./navbar";
44
import { Sidebar } from "./sidebar";
55
import { ReactNode } from "react";
66
import { PyodideProvider } from "./terminal/python/pyodide";
7+
import { FileProvider } from "./terminal/file";
78

89
export const metadata: Metadata = {
910
title: "Create Next App",
@@ -20,7 +21,9 @@ export default function RootLayout({
2021
<input id="drawer-toggle" type="checkbox" className="drawer-toggle" />
2122
<div className="drawer-content flex flex-col">
2223
<Navbar />
23-
<PyodideProvider>{children}</PyodideProvider>
24+
<PyodideProvider>
25+
<FileProvider>{children}</FileProvider>
26+
</PyodideProvider>
2427
</div>
2528
<div className="drawer-side shadow-md">
2629
<label

app/terminal/editor.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import { useFile } from "./file";
4+
import AceEditor from "react-ace";
5+
// テーマは色分けが今のTerminal側のハイライト(highlight.js)の実装に近いものを適当に選んだ
6+
import "ace-builds/src-min-noconflict/theme-tomorrow";
7+
import "ace-builds/src-min-noconflict/theme-twilight";
8+
import "ace-builds/src-min-noconflict/ext-language_tools";
9+
import "ace-builds/src-min-noconflict/ext-searchbox";
10+
import "ace-builds/src-min-noconflict/mode-python";
11+
// snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python";
12+
13+
interface EditorProps {
14+
language?: string;
15+
tabSize: number;
16+
filename: string;
17+
initContent: string;
18+
}
19+
export function EditorComponent(props: EditorProps) {
20+
const { files, writeFile } = useFile();
21+
const code = files[props.filename] || props.initContent;
22+
23+
return (
24+
<div>
25+
<div className="font-mono text-sm mt-2 ml-4 ">{props.filename}</div>
26+
<AceEditor
27+
name={`ace-editor-${props.filename}`}
28+
mode={props.language}
29+
theme="tomorrow" // TODO dark theme
30+
tabSize={props.tabSize}
31+
width="100%"
32+
height={
33+
Math.max((props.initContent.split("\n").length + 2) * 14, 128) + "px"
34+
}
35+
className="font-mono!" // Aceのデフォルトフォントを上書き
36+
fontSize={14}
37+
enableBasicAutocompletion={true}
38+
enableLiveAutocompletion={true}
39+
enableSnippets={false}
40+
value={code}
41+
onChange={(code: string) => writeFile(props.filename, code)}
42+
/>
43+
</div>
44+
);
45+
}

app/terminal/file.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import {
4+
createContext,
5+
ReactNode,
6+
useCallback,
7+
useContext,
8+
useState,
9+
} from "react";
10+
11+
/*
12+
ファイルの内容を1箇所でまとめて管理する。
13+
const [code, setCode] = useState("")
14+
の代わりに
15+
const {files, writeFile} = useFile();
16+
files["ファイル名"]
17+
でどこからでも同じファイルの中身を取得でき、
18+
writeFile() で書き込むこともできる。
19+
*/
20+
interface IFileContext {
21+
files: Record<string, string | undefined>;
22+
writeFile: (name: string, content: string) => void;
23+
}
24+
const FileContext = createContext<IFileContext>(null!);
25+
26+
export const useFile = () => useContext(FileContext);
27+
28+
export function FileProvider({ children }: { children: ReactNode }) {
29+
const [files, setFiles] = useState<Record<string, string>>({});
30+
const writeFile = useCallback((name: string, content: string) => {
31+
setFiles((files) => {
32+
files[name] = content;
33+
return { ...files };
34+
});
35+
}, []);
36+
37+
return (
38+
<FileContext.Provider value={{ files, writeFile }}>
39+
{children}
40+
</FileContext.Provider>
41+
);
42+
}

app/terminal/python/embedded.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
import { useMemo } from "react";
44
import { TerminalComponent, TerminalOutput } from "../terminal";
55
import { usePyodide } from "./pyodide";
6+
import { useFile } from "../file";
7+
8+
interface ExecProps {
9+
filename: string;
10+
content: string;
11+
}
12+
export function PythonExecFile(props: ExecProps){
13+
const {files} = useFile();
14+
15+
return <>
16+
<div>
17+
<button className="btn btn-soft btn-primary">
18+
▶ 実行
19+
</button>
20+
<code className="text-sm">
21+
python {props.filename}
22+
</code>
23+
</div>
24+
</>
25+
}
626

727
export function PythonEmbeddedTerminal({ content }: { content: string }) {
828
const initCommands = useMemo(() => splitContents(content), [content]);

app/terminal/python/page.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"use client";
22

3+
import { EditorComponent } from "../editor";
34
import { TerminalComponent } from "../terminal";
45
import { usePyodide } from "./pyodide";
56

67
export default function PythonPage() {
7-
const { init, ready, initializing, runPython, checkSyntax, mutex } = usePyodide();
8+
const { init, ready, initializing, runPython, checkSyntax, mutex } =
9+
usePyodide();
810
return (
9-
<div className="p-4">
11+
<div className="p-4 flex flex-col gap-4">
1012
<TerminalComponent
1113
initRuntime={init}
1214
runtimeInitializing={initializing}
@@ -20,6 +22,12 @@ export default function PythonPage() {
2022
sendCommand={runPython}
2123
checkSyntax={checkSyntax}
2224
/>
25+
<EditorComponent
26+
language="python"
27+
tabSize={4}
28+
filename="main.py"
29+
initContent="print('hello, world!')"
30+
/>
2331
</div>
2432
);
2533
}

app/terminal/terminal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export function TerminalComponent(props: TerminalComponentProps) {
207207
// fitAddon.fit();
208208
const dims = fitAddon.proposeDimensions();
209209
if (dims) {
210-
const rows = getRowsOfInitCommand.current(dims.cols);
210+
const rows = Math.max(5, getRowsOfInitCommand.current(dims.cols));
211211
term.resize(dims.cols, rows);
212212
setCurrentRows(rows);
213213
}
@@ -356,7 +356,7 @@ export function TerminalComponent(props: TerminalComponentProps) {
356356

357357
return (
358358
<div
359-
className="relative p-4 bg-base-300 min-h-32 h-max"
359+
className="relative h-max"
360360
onClick={() => {
361361
if (
362362
!runtimeInitializing &&

0 commit comments

Comments
 (0)