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/editable.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 { Input } from "antd";
7
import { ReactNode, useEffect, useRef, useState } from "react";
8
import { useIsMountedRef } from "@cocalc/frontend/app-framework";
9
import { register, RenderElementProps } from "../register";
10
import { useSlate } from "../hooks";
11
import { SlateCodeMirror } from "../codemirror";
12
import { delay } from "awaiting";
13
import { useSetElement } from "../set-element";
14
import infoToMode from "./info-to-mode";
15
import ActionButtons, { RunFunction } from "./action-buttons";
16
import { useChange } from "../../use-change";
17
import { getHistory, isPreviousSiblingCodeBlock } from "./history";
18
import InsertBar from "./insert-bar";
19
import { useFileContext } from "@cocalc/frontend/lib/file-context";
20
import { isEqual } from "lodash";
21
import Mermaid from "./mermaid";
22
23
function Element({ attributes, children, element }: RenderElementProps) {
24
if (element.type != "code_block") {
25
throw Error("bug");
26
}
27
const { disableMarkdownCodebar } = useFileContext();
28
const editor = useSlate();
29
const isMountedRef = useIsMountedRef();
30
const [info, setInfo] = useState<string>(element.info ?? "");
31
const infoFocusedRef = useRef<boolean>(false);
32
const [output, setOutput] = useState<null | ReactNode>(null);
33
const runRef = useRef<RunFunction | null>(null);
34
const setElement = useSetElement(editor, element);
35
// textIndent: 0 is needed due to task lists -- see https://github.com/sagemathinc/cocalc/issues/6074
36
const { change } = useChange();
37
const [history, setHistory] = useState<string[]>(
38
getHistory(editor, element) ?? [],
39
);
40
const [codeSibling, setCodeSibling] = useState<boolean>(
41
isPreviousSiblingCodeBlock(editor, element),
42
);
43
useEffect(() => {
44
const newHistory = getHistory(editor, element);
45
if (newHistory != null && !isEqual(history, newHistory)) {
46
setHistory(newHistory);
47
setCodeSibling(isPreviousSiblingCodeBlock(editor, element));
48
}
49
if (!infoFocusedRef.current && element.info != info) {
50
// upstream change
51
setInfo(element.info);
52
}
53
}, [change, element]);
54
55
return (
56
<div {...attributes}>
57
<div contentEditable={false} style={{ textIndent: 0 }}>
58
{!codeSibling && (
59
<InsertBar
60
editor={editor}
61
element={element}
62
info={info}
63
above={true}
64
/>
65
)}
66
<div style={{ display: "flex", flexDirection: "column" }}>
67
<div style={{ flex: 1 }}>
68
<SlateCodeMirror
69
options={{ lineWrapping: true }}
70
value={element.value}
71
info={infoToMode(element.info, { value: element.value })}
72
onChange={(value) => {
73
setElement({ value });
74
}}
75
onFocus={async () => {
76
await delay(1); // must be a little longer than the onBlur below.
77
if (!isMountedRef.current) return;
78
}}
79
onBlur={async () => {
80
await delay(0);
81
if (!isMountedRef.current) return;
82
}}
83
onShiftEnter={() => {
84
runRef.current?.();
85
}}
86
addonBefore={
87
<div
88
style={{
89
borderBottom: "1px solid #ccc",
90
padding: "3px",
91
display: "flex",
92
background: "#f8f8f8",
93
}}
94
>
95
<div style={{ flex: 1 }}></div>
96
{element.fence && (
97
<Input
98
size="small"
99
onKeyDown={(e) => {
100
if (e.keyCode == 13 && e.shiftKey) {
101
runRef.current?.();
102
} else if (e.keyCode == 40) {
103
// down arrow and 38 is up. TODO
104
}
105
}}
106
style={{
107
flex: 1,
108
color: "#666",
109
minWidth: "100px",
110
maxWidth: "300px",
111
margin: "0 5px",
112
}}
113
placeholder="Info string (py, r, jl, tex, md, etc.)..."
114
value={info}
115
onFocus={() => {
116
infoFocusedRef.current = true;
117
editor.setIgnoreSelection(true);
118
}}
119
onBlur={() => {
120
infoFocusedRef.current = false;
121
editor.setIgnoreSelection(false);
122
}}
123
onChange={(e) => {
124
const info = e.target.value;
125
setInfo(info);
126
setElement({ info });
127
}}
128
/>
129
)}
130
{!disableMarkdownCodebar && (
131
<ActionButtons
132
auto
133
size="small"
134
input={element.value}
135
history={history}
136
setOutput={setOutput}
137
output={output}
138
info={info}
139
runRef={runRef}
140
setInfo={(info) => {
141
setElement({ info });
142
}}
143
/>
144
)}
145
</div>
146
}
147
addonAfter={
148
disableMarkdownCodebar || output == null ? null : (
149
<div
150
onMouseDown={() => {
151
editor.setIgnoreSelection(true);
152
}}
153
onMouseUp={() => {
154
// Re-enable slate listing for selection changes again in next render loop.
155
setTimeout(() => {
156
editor.setIgnoreSelection(false);
157
}, 0);
158
}}
159
style={{
160
borderTop: "1px dashed #ccc",
161
background: "white",
162
padding: "5px 0 5px 30px",
163
}}
164
>
165
{output}
166
</div>
167
)
168
}
169
/>
170
</div>
171
{element.info == "mermaid" && (
172
<Mermaid style={{ flex: 1 }} value={element.value} />
173
)}
174
</div>
175
<InsertBar
176
editor={editor}
177
element={element}
178
info={info}
179
above={false}
180
/>
181
</div>
182
{children}
183
</div>
184
);
185
}
186
187
function fromSlate({ node }) {
188
const value = node.value as string;
189
190
// We always convert them to fenced, because otherwise collaborative editing just
191
// isn't possible, e.g., because you can't have blank lines at the end. This isn't
192
// too bad, since the conversion only happens for code blocks you actually touch.
193
if (true || node.fence) {
194
const info = node.info.trim() ?? "";
195
// There is one special case with fenced codeblocks that we
196
// have to worry about -- if they contain ```, then we need
197
// to wrap with *more* than the max sequence of backticks
198
// actually in the codeblock! See
199
// https://stackoverflow.com/questions/49267811/how-can-i-escape-3-backticks-code-block-in-3-backticks-code-block
200
// for an excellent discussion of this, and also
201
// https://github.com/mwouts/jupytext/issues/712
202
let fence = "```";
203
while (value.indexOf(fence) != -1) {
204
fence += "`";
205
}
206
return fence + info + "\n" + value + "\n" + fence + "\n\n";
207
// this was the old code for non-fenced blocks:
208
// } else {
209
// return indent(value, 4) + "\n\n";
210
}
211
}
212
213
register({
214
slateType: "code_block",
215
fromSlate,
216
Element,
217
rules: {
218
autoFocus: true,
219
autoAdvance: true,
220
},
221
});
222
223