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