| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 | /* eslint-disable @typescript-eslint/ban-ts-comment */
import styled from "styles"
import { useStateDesigner } from "@state-designer/react"
import React, { useEffect, useRef } from "react"
import { motion } from "framer-motion"
import state, { useSelector } from "state"
import { CodeFile } from "types"
import CodeDocs from "./code-docs"
import CodeEditor from "./code-editor"
import { generateFromCode } from "lib/code/generate"
import * as Panel from "../panel"
import { IconButton } from "../shared"
import {
  X,
  Code,
  Info,
  PlayCircle,
  ChevronUp,
  ChevronDown,
} from "react-feather"
const getErrorLineAndColumn = (e: any) => {
  if ("line" in e) {
    return { line: Number(e.line), column: e.column }
  }
  const result = e.stack.match(/:([0-9]+):([0-9]+)/)
  if (result) {
    return { line: Number(result[1]) - 1, column: result[2] }
  }
}
export default function CodePanel() {
  const rContainer = useRef<HTMLDivElement>(null)
  const isReadOnly = useSelector((s) => s.data.isReadOnly)
  const fileId = useSelector((s) => s.data.currentCodeFileId)
  const file = useSelector(
    (s) => s.data.document.code[s.data.currentCodeFileId]
  )
  const isOpen = useSelector((s) => s.data.settings.isCodeOpen)
  const fontSize = useSelector((s) => s.data.settings.fontSize)
  const local = useStateDesigner({
    data: {
      code: file.code,
      error: null as { message: string; line: number; column: number } | null,
    },
    on: {
      MOUNTED: "setCode",
      CHANGED_FILE: "loadFile",
    },
    initial: "editingCode",
    states: {
      editingCode: {
        on: {
          RAN_CODE: ["saveCode", "runCode"],
          SAVED_CODE: ["saveCode", "runCode"],
          CHANGED_CODE: { secretlyDo: "setCode" },
          CLEARED_ERROR: { if: "hasError", do: "clearError" },
          TOGGLED_DOCS: { to: "viewingDocs" },
        },
      },
      viewingDocs: {
        on: {
          TOGGLED_DOCS: { to: "editingCode" },
        },
      },
    },
    conditions: {
      hasError(data) {
        return !!data.error
      },
    },
    actions: {
      loadFile(data, payload: { file: CodeFile }) {
        data.code = payload.file.code
      },
      setCode(data, payload: { code: string }) {
        data.code = payload.code
      },
      runCode(data) {
        let error = null
        try {
          const { shapes, controls } = generateFromCode(data.code)
          state.send("GENERATED_FROM_CODE", { shapes, controls })
        } catch (e) {
          console.error(e)
          error = { message: e.message, ...getErrorLineAndColumn(e) }
        }
        data.error = error
      },
      saveCode(data) {
        const { code } = data
        state.send("SAVED_CODE", { code })
      },
      clearError(data) {
        data.error = null
      },
    },
  })
  useEffect(() => {
    local.send("CHANGED_FILE", { file })
  }, [file])
  useEffect(() => {
    local.send("MOUNTED", { code: state.data.document.code[fileId].code })
    return () => {
      state.send("CHANGED_CODE", { fileId, code: local.data.code })
    }
  }, [])
  const { error } = local.data
  return (
    <Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
      {isOpen ? (
        <Panel.Layout>
          <Panel.Header>
            <IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
              <X />
            </IconButton>
            <h3>Code</h3>
            <ButtonsGroup>
              <FontSizeButtons>
                <IconButton
                  disabled={!local.isIn("editingCode")}
                  onClick={() => state.send("INCREASED_CODE_FONT_SIZE")}
                >
                  <ChevronUp />
                </IconButton>
                <IconButton
                  disabled={!local.isIn("editingCode")}
                  onClick={() => state.send("DECREASED_CODE_FONT_SIZE")}
                >
                  <ChevronDown />
                </IconButton>
              </FontSizeButtons>
              <IconButton onClick={() => local.send("TOGGLED_DOCS")}>
                <Info />
              </IconButton>
              <IconButton
                disabled={!local.isIn("editingCode")}
                onClick={() => local.send("SAVED_CODE")}
              >
                <PlayCircle />
              </IconButton>
            </ButtonsGroup>
          </Panel.Header>
          <Panel.Content>
            <CodeEditor
              fontSize={fontSize}
              readOnly={isReadOnly}
              value={file.code}
              error={error}
              onChange={(code) => local.send("CHANGED_CODE", { code })}
              onSave={() => local.send("SAVED_CODE")}
              onKey={() => local.send("CLEARED_ERROR")}
            />
            <CodeDocs isHidden={!local.isIn("viewingDocs")} />
          </Panel.Content>
          <Panel.Footer>
            {error &&
              (error.line
                ? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
                : error.message)}
          </Panel.Footer>
        </Panel.Layout>
      ) : (
        <IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}>
          <Code />
        </IconButton>
      )}
    </Panel.Root>
  )
}
const ButtonsGroup = styled("div", {
  gridRow: "1",
  gridColumn: "3",
  display: "flex",
})
const FontSizeButtons = styled("div", {
  paddingRight: 4,
  display: "flex",
  flexDirection: "column",
  "& > button": {
    height: "50%",
    "&:nth-of-type(1)": {
      alignItems: "flex-end",
    },
    "&:nth-of-type(2)": {
      alignItems: "flex-start",
    },
    "& svg": {
      height: 12,
    },
  },
})
 |