UNPKG

@sanity/code-input

Version:

Sanity input component for code, powered by CodeMirror

418 lines (417 loc) • 16.3 kB
import { jsx } from "react/jsx-runtime"; import { Decoration, EditorView, lineNumbers } from "@codemirror/view"; import { useRootTheme, useTheme, rem } from "@sanity/ui"; import CodeMirror from "@uiw/react-codemirror"; import { useMemo, forwardRef, useState, useEffect, useCallback, useContext } from "react"; import { CodeInputConfigContext } from "./index.js"; import { StreamLanguage } from "@codemirror/language"; import { StateEffect, StateField } from "@codemirror/state"; import { rgba } from "@sanity/ui/theme"; import { tags } from "@lezer/highlight"; import { createTheme } from "@uiw/codemirror-themes"; const defaultCodeModes = [ { name: "groq", loader: () => import("@codemirror/lang-javascript").then(({ javascriptLanguage }) => javascriptLanguage) }, { name: "javascript", loader: () => import("@codemirror/lang-javascript").then(({ javascript }) => javascript({ jsx: !1 })) }, { name: "jsx", loader: () => import("@codemirror/lang-javascript").then(({ javascript }) => javascript({ jsx: !0 })) }, { name: "typescript", loader: () => import("@codemirror/lang-javascript").then( ({ javascript }) => javascript({ jsx: !1, typescript: !0 }) ) }, { name: "tsx", loader: () => import("@codemirror/lang-javascript").then( ({ javascript }) => javascript({ jsx: !0, typescript: !0 }) ) }, { name: "php", loader: () => import("@codemirror/lang-php").then(({ php }) => php()) }, { name: "sql", loader: () => import("@codemirror/lang-sql").then(({ sql }) => sql()) }, { name: "mysql", loader: () => import("@codemirror/lang-sql").then(({ sql, MySQL }) => sql({ dialect: MySQL })) }, { name: "json", loader: () => import("@codemirror/lang-json").then(({ json }) => json()) }, { name: "markdown", loader: () => import("@codemirror/lang-markdown").then(({ markdown }) => markdown()) }, { name: "java", loader: () => import("@codemirror/lang-java").then(({ java }) => java()) }, { name: "html", loader: () => import("@codemirror/lang-html").then(({ html }) => html()) }, { name: "csharp", loader: () => import("@codemirror/legacy-modes/mode/clike").then( ({ csharp }) => StreamLanguage.define(csharp) ) }, { name: "sh", loader: () => import("@codemirror/legacy-modes/mode/shell").then(({ shell }) => StreamLanguage.define(shell)) }, { name: "css", loader: () => import("@codemirror/legacy-modes/mode/css").then(({ css }) => StreamLanguage.define(css)) }, { name: "scss", loader: () => import("@codemirror/legacy-modes/mode/css").then(({ css }) => StreamLanguage.define(css)) }, { name: "sass", loader: () => import("@codemirror/legacy-modes/mode/sass").then(({ sass }) => StreamLanguage.define(sass)) }, { name: "ruby", loader: () => import("@codemirror/legacy-modes/mode/ruby").then(({ ruby }) => StreamLanguage.define(ruby)) }, { name: "python", loader: () => import("@codemirror/legacy-modes/mode/python").then( ({ python }) => StreamLanguage.define(python) ) }, { name: "xml", loader: () => import("@codemirror/legacy-modes/mode/xml").then(({ xml }) => StreamLanguage.define(xml)) }, { name: "yaml", loader: () => import("@codemirror/legacy-modes/mode/yaml").then(({ yaml }) => StreamLanguage.define(yaml)) }, { name: "golang", loader: () => import("@codemirror/legacy-modes/mode/go").then(({ go }) => StreamLanguage.define(go)) }, { name: "text", loader: () => { } }, { name: "batch", loader: () => { } } ]; function getBackwardsCompatibleTone(themeCtx) { return themeCtx.tone !== "neutral" && themeCtx.tone !== "suggest" ? themeCtx.tone : themeCtx.tone === "neutral" ? "default" : "primary"; } const highlightLineClass = "cm-highlight-line", addLineHighlight = StateEffect.define(), removeLineHighlight = StateEffect.define(), lineHighlightField = StateField.define({ create() { return Decoration.none; }, update(lines, tr) { lines = lines.map(tr.changes); for (const e of tr.effects) e.is(addLineHighlight) && (lines = lines.update({ add: [lineHighlightMark.range(e.value)] })), e.is(removeLineHighlight) && (lines = lines.update({ filter: (from) => from !== e.value })); return lines; }, toJSON(value, state) { const highlightLines = [], iter = value.iter(); for (; iter.value; ) { const lineNumber = state.doc.lineAt(iter.from).number; highlightLines.includes(lineNumber) || highlightLines.push(lineNumber), iter.next(); } return highlightLines; }, fromJSON(value, state) { const lines = state.doc.lines, highlights = value.filter((line) => line <= lines).map((line) => lineHighlightMark.range(state.doc.line(line).from)); highlights.sort((a, b) => a.from - b.from); try { return Decoration.none.update({ add: highlights }); } catch (e) { return console.error(e), Decoration.none; } }, provide: (f) => EditorView.decorations.from(f) }), lineHighlightMark = Decoration.line({ class: highlightLineClass }), highlightState = { highlight: lineHighlightField }; function createCodeMirrorTheme(options) { const { themeCtx } = options, fallbackTone = getBackwardsCompatibleTone(themeCtx), dark = { color: themeCtx.theme.color.dark[fallbackTone] }, light = { color: themeCtx.theme.color.light[fallbackTone] }; return EditorView.baseTheme({ ".cm-lineNumbers": { cursor: "default" }, ".cm-line.cm-line": { position: "relative" }, // need set background with pseudoelement so it does not render over selection color [`.${highlightLineClass}::before`]: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0, zIndex: -3, content: "''", boxSizing: "border-box" }, [`&dark .${highlightLineClass}::before`]: { background: rgba(dark.color.muted.caution.pressed.bg, 0.5) }, [`&light .${highlightLineClass}::before`]: { background: rgba(light.color.muted.caution.pressed.bg, 0.75) } }); } const highlightLine = (config) => { const highlightTheme = createCodeMirrorTheme({ themeCtx: config.theme }); return [ lineHighlightField, config.readOnly ? [] : lineNumbers({ domEventHandlers: { mousedown: (editorView, lineInfo) => { const line = editorView.state.doc.lineAt(lineInfo.from); let isHighlighted = !1; return editorView.state.field(lineHighlightField).between(line.from, line.to, (from, to, value) => { if (value) return isHighlighted = !0, !1; }), isHighlighted ? editorView.dispatch({ effects: removeLineHighlight.of(line.from) }) : editorView.dispatch({ effects: addLineHighlight.of(line.from) }), config != null && config.onHighlightChange && config.onHighlightChange(editorView.state.toJSON(highlightState).highlight), !0; } } }), highlightTheme ]; }; function setHighlightedLines(view, highlightLines) { const doc = view.state.doc, lines = doc.lines, allLineNumbers = Array.from({ length: lines }, (x, i) => i + 1); view.dispatch({ effects: allLineNumbers.map((lineNumber) => { const line = doc.line(lineNumber); return highlightLines != null && highlightLines.includes(lineNumber) ? addLineHighlight.of(line.from) : removeLineHighlight.of(line.from); }) }); } function useThemeExtension() { const themeCtx = useRootTheme(); return useMemo(() => { const fallbackTone = getBackwardsCompatibleTone(themeCtx), dark = { color: themeCtx.theme.color.dark[fallbackTone] }, light = { color: themeCtx.theme.color.light[fallbackTone] }; return EditorView.baseTheme({ "&.cm-editor": { height: "100%" }, "&.cm-editor.cm-focused": { outline: "none" }, // Matching brackets "&.cm-editor.cm-focused .cm-matchingBracket": { backgroundColor: "transparent" }, "&.cm-editor.cm-focused .cm-nonmatchingBracket": { backgroundColor: "transparent" }, "&dark.cm-editor.cm-focused .cm-matchingBracket": { outline: `1px solid ${dark.color.base.border}` }, "&dark.cm-editor.cm-focused .cm-nonmatchingBracket": { outline: `1px solid ${dark.color.base.border}` }, "&light.cm-editor.cm-focused .cm-matchingBracket": { outline: `1px solid ${light.color.base.border}` }, "&light.cm-editor.cm-focused .cm-nonmatchingBracket": { outline: `1px solid ${light.color.base.border}` }, // Size and padding of gutter "& .cm-lineNumbers .cm-gutterElement": { minWidth: "32px !important", padding: "0 8px !important" }, "& .cm-gutter.cm-foldGutter": { width: "0px !important" }, // Color of gutter "&dark .cm-gutters": { color: `${rgba(dark.color.card.enabled.code.fg, 0.5)} !important`, borderRight: `1px solid ${rgba(dark.color.base.border, 0.5)}` }, "&light .cm-gutters": { color: `${rgba(light.color.card.enabled.code.fg, 0.5)} !important`, borderRight: `1px solid ${rgba(light.color.base.border, 0.5)}` } }); }, [themeCtx]); } function useCodeMirrorTheme() { const theme = useTheme(); return useMemo(() => { const { code: codeFont } = theme.sanity.fonts, { base, card, dark, syntax } = theme.sanity.color; return createTheme({ theme: dark ? "dark" : "light", settings: { background: card.enabled.bg, foreground: card.enabled.code.fg, lineHighlight: card.enabled.bg, fontFamily: codeFont.family, caret: base.focusRing, selection: rgba(base.focusRing, 0.2), selectionMatch: rgba(base.focusRing, 0.4), gutterBackground: card.disabled.bg, gutterForeground: card.disabled.code.fg, gutterActiveForeground: card.enabled.fg }, styles: [ { tag: [tags.heading, tags.heading2, tags.heading3, tags.heading4, tags.heading5, tags.heading6], color: card.enabled.fg }, { tag: tags.angleBracket, color: card.enabled.code.fg }, { tag: tags.atom, color: syntax.keyword }, { tag: tags.attributeName, color: syntax.attrName }, { tag: tags.bool, color: syntax.boolean }, { tag: tags.bracket, color: card.enabled.code.fg }, { tag: tags.className, color: syntax.className }, { tag: tags.comment, color: syntax.comment }, { tag: tags.definition(tags.typeName), color: syntax.function }, { tag: [ tags.definition(tags.variableName), tags.function(tags.variableName), tags.className, tags.attributeName ], color: syntax.function }, { tag: [tags.function(tags.propertyName), tags.propertyName], color: syntax.function }, { tag: tags.keyword, color: syntax.keyword }, { tag: tags.null, color: syntax.number }, { tag: tags.number, color: syntax.number }, { tag: tags.meta, color: card.enabled.code.fg }, { tag: tags.operator, color: syntax.operator }, { tag: tags.propertyName, color: syntax.property }, { tag: [tags.string, tags.special(tags.brace)], color: syntax.string }, { tag: tags.tagName, color: syntax.className }, { tag: tags.typeName, color: syntax.keyword } ] }); }, [theme]); } function useFontSizeExtension(props) { const { fontSize: fontSizeProp } = props, theme = useTheme(); return useMemo(() => { const { code: codeFont } = theme.sanity.fonts, { fontSize, lineHeight } = codeFont.sizes[fontSizeProp] || codeFont.sizes[2]; return EditorView.baseTheme({ "&": { fontSize: rem(fontSize) }, "& .cm-scroller": { lineHeight: `${lineHeight / fontSize} !important` } }); }, [fontSizeProp, theme]); } var __defProp = Object.defineProperty, __defProps = Object.defineProperties, __getOwnPropDescs = Object.getOwnPropertyDescriptors, __getOwnPropSymbols = Object.getOwnPropertySymbols, __hasOwnProp = Object.prototype.hasOwnProperty, __propIsEnum = Object.prototype.propertyIsEnumerable, __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value }) : obj[key] = value, __spreadValues = (a, b) => { for (var prop in b || (b = {})) __hasOwnProp.call(b, prop) && __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) __propIsEnum.call(b, prop) && __defNormalProp(a, prop, b[prop]); return a; }, __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)), __objRest = (source, exclude) => { var target = {}; for (var prop in source) __hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0 && (target[prop] = source[prop]); if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop) && (target[prop] = source[prop]); return target; }; const CodeMirrorProxy = forwardRef( function(props, ref) { const _a = props, { basicSetup: basicSetupProp, highlightLines, languageMode, onHighlightChange, readOnly, value } = _a, codeMirrorProps = __objRest(_a, [ "basicSetup", "highlightLines", "languageMode", "onHighlightChange", "readOnly", "value" ]), themeCtx = useRootTheme(), codeMirrorTheme = useCodeMirrorTheme(), [editorView, setEditorView] = useState(void 0), themeExtension = useThemeExtension(), fontSizeExtension = useFontSizeExtension({ fontSize: 1 }), languageExtension = useLanguageExtension(languageMode), highlightLineExtension = useMemo( () => highlightLine({ onHighlightChange, readOnly, theme: themeCtx }), [onHighlightChange, readOnly, themeCtx] ), extensions = useMemo(() => { const baseExtensions = [ themeExtension, fontSizeExtension, highlightLineExtension, EditorView.lineWrapping ]; return languageExtension ? [...baseExtensions, languageExtension] : baseExtensions; }, [fontSizeExtension, highlightLineExtension, languageExtension, themeExtension]); useEffect(() => { editorView && setHighlightedLines(editorView, highlightLines != null ? highlightLines : []); }, [editorView, highlightLines, value]); const initialState = useMemo(() => ({ json: { doc: value != null ? value : "", selection: { main: 0, ranges: [{ anchor: 0, head: 0 }] }, highlight: highlightLines != null ? highlightLines : [] }, fields: highlightState }), []), handleCreateEditor = useCallback((view) => { setEditorView(view); }, []), basicSetup = useMemo( () => basicSetupProp != null ? basicSetupProp : { highlightActiveLine: !1 }, [basicSetupProp] ); return /* @__PURE__ */ jsx( CodeMirror, __spreadProps(__spreadValues({}, codeMirrorProps), { value, ref, extensions, theme: codeMirrorTheme, onCreateEditor: handleCreateEditor, initialState, basicSetup }) ); } ); function useLanguageExtension(mode) { const codeConfig = useContext(CodeInputConfigContext), [languageExtension, setLanguageExtension] = useState(); return useEffect(() => { var _a; const codeMode = [...(_a = codeConfig == null ? void 0 : codeConfig.codeModes) != null ? _a : [], ...defaultCodeModes].find((m) => m.name === mode); codeMode != null && codeMode.loader || console.warn( `Found no codeMode for language mode ${mode}, syntax highlighting will be disabled.` ); let active = !0; return Promise.resolve(codeMode == null ? void 0 : codeMode.loader()).then((extension) => { active && setLanguageExtension(extension); }).catch((e) => { console.error(`Failed to load language mode ${mode}`, e), active && setLanguageExtension(void 0); }), () => { active = !1; }; }, [mode, codeConfig]), languageExtension; } export { CodeMirrorProxy as default }; //# sourceMappingURL=CodeMirrorProxy.js.map