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