Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/editors/slate/elements/code-block/index.tsx
1698 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Button, Tooltip } from "antd";
7
import React, { ReactNode, useEffect, useRef, useState } from "react";
8
import { Element } from "slate";
9
import { register, SlateElement, RenderElementProps } from "../register";
10
import { CodeMirrorStatic } from "@cocalc/frontend/jupyter/codemirror-static";
11
import infoToMode from "./info-to-mode";
12
import ActionButtons from "./action-buttons";
13
import { useChange } from "../../use-change";
14
import { getHistory } from "./history";
15
import { DARK_GREY_BORDER } from "../../util";
16
import { useFileContext } from "@cocalc/frontend/lib/file-context";
17
import { Icon } from "@cocalc/frontend/components/icon";
18
import { isEqual } from "lodash";
19
import Mermaid from "./mermaid";
20
21
export interface CodeBlock extends SlateElement {
22
type: "code_block";
23
isVoid: true;
24
fence: boolean;
25
value: string;
26
info: string;
27
}
28
29
export const StaticElement: React.FC<RenderElementProps> = ({
30
attributes,
31
element,
32
}) => {
33
if (element.type != "code_block") {
34
throw Error("bug");
35
}
36
37
const { disableMarkdownCodebar, jupyterApiEnabled } = useFileContext();
38
39
// we need both a ref and state, because editing is used both for the UI
40
// state and also at once point directly to avoid saving the last change
41
// after doing shift+enter.
42
const editingRef = useRef<boolean>(false);
43
const [editing, setEditing0] = useState<boolean>(false);
44
const setEditing = (editing) => {
45
editingRef.current = editing;
46
setEditing0(editing);
47
};
48
49
const [newValue, setNewValue] = useState<string | null>(null);
50
const runRef = useRef<any>(null);
51
52
const [output, setOutput] = useState<null | ReactNode>(null);
53
54
const { change, editor, setEditor } = useChange();
55
const [history, setHistory] = useState<string[]>(
56
getHistory(editor, element) ?? [],
57
);
58
useEffect(() => {
59
const newHistory = getHistory(editor, element);
60
if (newHistory != null && !isEqual(history, newHistory)) {
61
setHistory(newHistory);
62
}
63
}, [change]);
64
65
const [temporaryInfo, setTemporaryInfo] = useState<string | null>(null);
66
useEffect(() => {
67
setTemporaryInfo(null);
68
}, [element.info]);
69
70
const save = (value: string | null, run: boolean) => {
71
setEditing(false);
72
if (value != null && setEditor != null && editor != null) {
73
// We just directly find it assuming it is a top level block for now.
74
// We aren't using the slate library since in static mode right now
75
// the editor isn't actually a slate editor object (yet).
76
const editor2 = { children: [...editor.children] };
77
for (let i = 0; i < editor2.children.length; i++) {
78
if (element === editor.children[i]) {
79
editor2.children[i] = { ...(element as any), value };
80
setEditor(editor2);
81
break;
82
}
83
}
84
}
85
if (!run) return;
86
// have to wait since above causes re-render
87
setTimeout(() => {
88
runRef.current?.();
89
}, 1);
90
};
91
92
const isMermaid = element.info == "mermaid";
93
if (isMermaid) {
94
return (
95
<div {...attributes} style={{ marginBottom: "1em", textIndent: 0 }}>
96
<Mermaid value={newValue ?? element.value} />
97
</div>
98
);
99
}
100
101
// textIndent: 0 is needed due to task lists -- see https://github.com/sagemathinc/cocalc/issues/6074
102
// editable since even CodeMirrorStatic is editable, but meant to be *ephemeral* editing.
103
return (
104
<div {...attributes} style={{ marginBottom: "1em", textIndent: 0 }}>
105
<CodeMirrorStatic
106
editable={editing}
107
onChange={(event) => {
108
if (!editingRef.current) return;
109
setNewValue(event.target.value);
110
}}
111
onKeyDown={(event) => {
112
if (event.shiftKey && event.keyCode === 13) {
113
save(newValue, true);
114
}
115
}}
116
onDoubleClick={() => {
117
setEditing(true);
118
}}
119
addonBefore={
120
!disableMarkdownCodebar && (
121
<div
122
style={{
123
borderBottom: "1px solid #ccc",
124
padding: "3px",
125
display: "flex",
126
background: "#f8f8f8",
127
}}
128
>
129
<div style={{ flex: 1 }}></div>
130
{jupyterApiEnabled && (
131
<Tooltip
132
title={
133
<>
134
Make a <i>temporary</i> change to this code.{" "}
135
<b>This is not saved permanently anywhere!</b>
136
</>
137
}
138
>
139
<Button
140
size="small"
141
type={
142
editing && newValue != element.value ? undefined : "text"
143
}
144
style={
145
editing && newValue != element.value
146
? { background: "#5cb85c", color: "white" }
147
: { color: "#666" }
148
}
149
onClick={() => {
150
if (editing) {
151
save(newValue, false);
152
} else {
153
setEditing(true);
154
}
155
}}
156
>
157
<Icon name={"pencil"} /> {editing ? "Save" : "Edit"}
158
</Button>{" "}
159
</Tooltip>
160
)}
161
<ActionButtons
162
auto
163
size="small"
164
runRef={runRef}
165
input={newValue ?? element.value}
166
history={history}
167
setOutput={setOutput}
168
output={output}
169
info={temporaryInfo ?? element.info}
170
setInfo={(info) => {
171
setTemporaryInfo(info);
172
}}
173
/>
174
</div>
175
)
176
}
177
value={newValue ?? element.value}
178
style={{
179
background: "white",
180
padding: "10px 15px 10px 20px",
181
borderLeft: `10px solid ${DARK_GREY_BORDER}`,
182
borderRadius: 0,
183
}}
184
options={{
185
mode: infoToMode(temporaryInfo ?? element.info, {
186
value: element.value,
187
}),
188
}}
189
addonAfter={
190
disableMarkdownCodebar || output == null ? null : (
191
<div
192
style={{
193
borderTop: "1px dashed #ccc",
194
background: "white",
195
padding: "5px 0 5px 30px",
196
}}
197
>
198
{output}
199
</div>
200
)
201
}
202
/>
203
</div>
204
);
205
};
206
207
export function toSlate({ token }) {
208
// fence =block of code with ``` around it, but not indented.
209
let value = token.content;
210
211
// We remove the last carriage return (right before ```), since it
212
// is much easier to do that here...
213
if (value[value.length - 1] == "\n") {
214
value = value.slice(0, value.length - 1);
215
}
216
const info = token.info ?? "";
217
if (typeof info != "string") {
218
throw Error("info must be a string");
219
}
220
return {
221
type: "code_block",
222
isVoid: true,
223
fence: token.type == "fence",
224
value,
225
info,
226
children: [{ text: "" }],
227
} as Element;
228
}
229
230
function sizeEstimator({ node, fontSize }): number {
231
return node.value.split("\n").length * (fontSize + 2) + 10 + fontSize;
232
}
233
234
register({
235
slateType: "code_block",
236
markdownType: ["fence", "code_block"],
237
StaticElement,
238
toSlate,
239
sizeEstimator,
240
});
241
242