@sanity/code-input 3.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +140 -64
  3. package/lib/_chunks/CodeMirrorProxy-3836f097.js +619 -0
  4. package/lib/_chunks/CodeMirrorProxy-3836f097.js.map +1 -0
  5. package/lib/_chunks/CodeMirrorProxy-e83d4d37.js +611 -0
  6. package/lib/_chunks/CodeMirrorProxy-e83d4d37.js.map +1 -0
  7. package/lib/_chunks/index-17e68aff.js +563 -0
  8. package/lib/_chunks/index-17e68aff.js.map +1 -0
  9. package/lib/_chunks/index-9a4cb814.js +549 -0
  10. package/lib/_chunks/index-9a4cb814.js.map +1 -0
  11. package/lib/{src/index.d.ts → index.d.ts} +18 -10
  12. package/lib/index.esm.js +1 -1
  13. package/lib/index.esm.js.map +1 -1
  14. package/lib/index.js +10 -1
  15. package/lib/index.js.map +1 -1
  16. package/package.json +53 -27
  17. package/src/CodeInput.tsx +72 -272
  18. package/src/LanguageField.tsx +33 -0
  19. package/src/LanguageInput.tsx +32 -0
  20. package/src/PreviewCode.tsx +40 -68
  21. package/src/__workshop__/index.ts +22 -0
  22. package/src/__workshop__/lazy.tsx +54 -0
  23. package/src/__workshop__/preview.tsx +24 -0
  24. package/src/__workshop__/props.tsx +24 -0
  25. package/src/codemirror/CodeMirrorProxy.tsx +157 -0
  26. package/src/codemirror/CodeModeContext.tsx +4 -0
  27. package/src/codemirror/defaultCodeModes.ts +109 -0
  28. package/src/codemirror/extensions/highlightLineExtension.ts +164 -0
  29. package/src/codemirror/extensions/theme.ts +61 -0
  30. package/src/codemirror/extensions/useCodeMirrorTheme.ts +63 -0
  31. package/src/codemirror/extensions/useFontSize.ts +24 -0
  32. package/src/codemirror/useCodeMirror-client.test.tsx +49 -0
  33. package/src/{ace-editor/AceEditor-server.test.tsx → codemirror/useCodeMirror-server.test.tsx} +3 -4
  34. package/src/codemirror/useCodeMirror.tsx +12 -0
  35. package/src/codemirror/useLanguageMode.tsx +52 -0
  36. package/src/config.ts +1 -13
  37. package/src/getMedia.tsx +0 -2
  38. package/src/index.ts +4 -11
  39. package/src/plugin.tsx +39 -0
  40. package/src/schema.tsx +3 -7
  41. package/src/types.ts +19 -3
  42. package/src/ui/focusRingStyle.ts +27 -0
  43. package/src/useFieldMember.ts +16 -0
  44. package/lib/_chunks/editorSupport-895caf32.esm.js +0 -2
  45. package/lib/_chunks/editorSupport-895caf32.esm.js.map +0 -1
  46. package/lib/_chunks/editorSupport-bda3d360.js +0 -2
  47. package/lib/_chunks/editorSupport-bda3d360.js.map +0 -1
  48. package/src/ace-editor/AceEditor-client.test.tsx +0 -37
  49. package/src/ace-editor/AceEditorLazy.tsx +0 -19
  50. package/src/ace-editor/editorSupport.ts +0 -34
  51. package/src/ace-editor/groq.ts +0 -630
  52. package/src/createHighlightMarkers.ts +0 -24
@@ -0,0 +1,24 @@
1
+ import {Box, Container} from '@sanity/ui'
2
+ import {useWorkshopSanity} from '@sanity/ui-workshop-plugin-sanity'
3
+ import {isObjectSchemaType, useSchema} from 'sanity'
4
+ import {DocumentFormProvider, SelectedInput} from 'sanity-extra'
5
+
6
+ export default function PreviewStory() {
7
+ const {onPatchEvent} = useWorkshopSanity()
8
+ const schema = useSchema()
9
+ const schemaType = schema.get('test')
10
+
11
+ if (!isObjectSchemaType(schemaType)) {
12
+ return <>Not an object type</>
13
+ }
14
+
15
+ return (
16
+ <DocumentFormProvider documentId="test" onPatchEvent={onPatchEvent} schemaType={schemaType}>
17
+ <Container width={1}>
18
+ <Box paddingX={4} paddingY={[5, 6, 7]}>
19
+ <SelectedInput selectedPath={['content']} />
20
+ </Box>
21
+ </Container>
22
+ </DocumentFormProvider>
23
+ )
24
+ }
@@ -0,0 +1,24 @@
1
+ import {Box, Container} from '@sanity/ui'
2
+ import {useWorkshopSanity} from '@sanity/ui-workshop-plugin-sanity'
3
+ import {isObjectSchemaType, useSchema} from 'sanity'
4
+ import {DocumentFormProvider, SelectedInput} from 'sanity-extra'
5
+
6
+ export default function PropsStory() {
7
+ const {onPatchEvent} = useWorkshopSanity()
8
+ const schema = useSchema()
9
+ const schemaType = schema.get('test')
10
+
11
+ if (!isObjectSchemaType(schemaType)) {
12
+ return <>Not an object type</>
13
+ }
14
+
15
+ return (
16
+ <DocumentFormProvider documentId="test" onPatchEvent={onPatchEvent} schemaType={schemaType}>
17
+ <Container width={1}>
18
+ <Box paddingX={4} paddingY={[5, 6, 7]}>
19
+ <SelectedInput selectedPath={['code']} />
20
+ </Box>
21
+ </Container>
22
+ </DocumentFormProvider>
23
+ )
24
+ }
@@ -0,0 +1,157 @@
1
+ import {forwardRef, useCallback, useContext, useEffect, useMemo, useState} from 'react'
2
+ import CodeMirror, {ReactCodeMirrorProps, ReactCodeMirrorRef} from '@uiw/react-codemirror'
3
+ import {useCodeMirrorTheme} from './extensions/useCodeMirrorTheme'
4
+ import {Extension} from '@codemirror/state'
5
+ import {CodeInputConfigContext} from './CodeModeContext'
6
+ import {defaultCodeModes} from './defaultCodeModes'
7
+ import {
8
+ highlightLine,
9
+ highlightState,
10
+ setHighlightedLines,
11
+ } from './extensions/highlightLineExtension'
12
+ import {EditorView} from '@codemirror/view'
13
+ import {useRootTheme} from '@sanity/ui'
14
+ import {useFontSizeExtension} from './extensions/useFontSize'
15
+ import {useThemeExtension} from './extensions/theme'
16
+
17
+ export interface CodeMirrorProps extends ReactCodeMirrorProps {
18
+ highlightLines?: number[]
19
+ languageMode?: string
20
+ onHighlightChange?: (lines: number[]) => void
21
+ }
22
+
23
+ /**
24
+ * CodeMirrorProxy is a wrapper component around CodeMirror that we lazy load to reduce initial bundle size.
25
+ *
26
+ * It is also responsible for integrating any CodeMirror extensions.
27
+ */
28
+ const CodeMirrorProxy = forwardRef<ReactCodeMirrorRef, CodeMirrorProps>(function CodeMirrorProxy(
29
+ props,
30
+ ref
31
+ ) {
32
+ const {
33
+ basicSetup: basicSetupProp,
34
+ highlightLines,
35
+ languageMode,
36
+ onHighlightChange,
37
+ readOnly,
38
+ value,
39
+ ...codeMirrorProps
40
+ } = props
41
+
42
+ const themeCtx = useRootTheme()
43
+ const codeMirrorTheme = useCodeMirrorTheme()
44
+ const [editorView, setEditorView] = useState<EditorView | undefined>(undefined)
45
+
46
+ // Resolve extensions
47
+ const themeExtension = useThemeExtension()
48
+ const fontSizeExtension = useFontSizeExtension({fontSize: 1})
49
+ const languageExtension = useLanguageExtension(languageMode)
50
+ const highlightLineExtension = useMemo(
51
+ () =>
52
+ highlightLine({
53
+ onHighlightChange,
54
+ readOnly,
55
+ theme: themeCtx,
56
+ }),
57
+ [onHighlightChange, readOnly, themeCtx]
58
+ )
59
+
60
+ const extensions = useMemo(() => {
61
+ const baseExtensions = [
62
+ themeExtension,
63
+ fontSizeExtension,
64
+ highlightLineExtension,
65
+ EditorView.lineWrapping,
66
+ ]
67
+ if (languageExtension) {
68
+ return [...baseExtensions, languageExtension]
69
+ }
70
+ return baseExtensions
71
+ }, [fontSizeExtension, highlightLineExtension, languageExtension, themeExtension])
72
+
73
+ useEffect(() => {
74
+ if (editorView) {
75
+ setHighlightedLines(editorView, highlightLines ?? [])
76
+ }
77
+ }, [editorView, highlightLines, value])
78
+
79
+ const initialState = useMemo(() => {
80
+ return {
81
+ json: {
82
+ doc: value ?? '',
83
+ selection: {
84
+ main: 0,
85
+ ranges: [{anchor: 0, head: 0}],
86
+ },
87
+ highlight: highlightLines ?? [],
88
+ },
89
+ fields: highlightState,
90
+ }
91
+ // only need to calculate this on initial render
92
+ // eslint-disable-next-line react-hooks/exhaustive-deps
93
+ }, [])
94
+
95
+ const handleCreateEditor = useCallback((view: EditorView) => {
96
+ setEditorView(view)
97
+ }, [])
98
+
99
+ const basicSetup = useMemo(
100
+ () =>
101
+ basicSetupProp ?? {
102
+ highlightActiveLine: false,
103
+ },
104
+ [basicSetupProp]
105
+ )
106
+
107
+ return (
108
+ <CodeMirror
109
+ {...codeMirrorProps}
110
+ value={value}
111
+ ref={ref}
112
+ extensions={extensions}
113
+ theme={codeMirrorTheme}
114
+ onCreateEditor={handleCreateEditor}
115
+ initialState={initialState}
116
+ basicSetup={basicSetup}
117
+ />
118
+ )
119
+ })
120
+
121
+ function useLanguageExtension(mode?: string) {
122
+ const codeConfig = useContext(CodeInputConfigContext)
123
+
124
+ const [languageExtension, setLanguageExtension] = useState<Extension | undefined>()
125
+
126
+ useEffect(() => {
127
+ const customModes = codeConfig?.codeModes ?? []
128
+ const modes = [...customModes, ...defaultCodeModes]
129
+
130
+ const codeMode = modes.find((m) => m.name === mode)
131
+ if (!codeMode?.loader) {
132
+ console.warn(
133
+ `Found no codeMode for language mode ${mode}, syntax highlighting will be disabled.`
134
+ )
135
+ }
136
+ let active = true
137
+ Promise.resolve(codeMode?.loader())
138
+ .then((extension) => {
139
+ if (active) {
140
+ setLanguageExtension(extension)
141
+ }
142
+ })
143
+ .catch((e) => {
144
+ console.error(`Failed to load language mode ${mode}`, e)
145
+ if (active) {
146
+ setLanguageExtension(undefined)
147
+ }
148
+ })
149
+ return () => {
150
+ active = false
151
+ }
152
+ }, [mode, codeConfig])
153
+
154
+ return languageExtension
155
+ }
156
+
157
+ export default CodeMirrorProxy
@@ -0,0 +1,4 @@
1
+ import {createContext} from 'react'
2
+ import {CodeInputConfig} from '../plugin'
3
+
4
+ export const CodeInputConfigContext = createContext<CodeInputConfig | undefined>(undefined)
@@ -0,0 +1,109 @@
1
+ import {StreamLanguage} from '@codemirror/language'
2
+ import {type Extension} from '@codemirror/state'
3
+
4
+ export interface CodeMode {
5
+ name: string
6
+ loader: ModeLoader
7
+ }
8
+ export type ModeLoader = () => Promise<Extension | undefined> | Extension | undefined
9
+
10
+ export const defaultCodeModes: CodeMode[] = [
11
+ {
12
+ name: 'groq',
13
+ loader: () =>
14
+ import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: false})),
15
+ },
16
+ {
17
+ name: 'javascript',
18
+ loader: () =>
19
+ import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: false})),
20
+ },
21
+ {
22
+ name: 'jsx',
23
+ loader: () =>
24
+ import('@codemirror/lang-javascript').then(({javascript}) => javascript({jsx: true})),
25
+ },
26
+ {
27
+ name: 'typescript',
28
+ loader: () =>
29
+ import('@codemirror/lang-javascript').then(({javascript}) =>
30
+ javascript({jsx: false, typescript: true})
31
+ ),
32
+ },
33
+ {
34
+ name: 'tsx',
35
+ loader: () =>
36
+ import('@codemirror/lang-javascript').then(({javascript}) =>
37
+ javascript({jsx: true, typescript: true})
38
+ ),
39
+ },
40
+ {name: 'php', loader: () => import('@codemirror/lang-php').then(({php}) => php())},
41
+ {name: 'sql', loader: () => import('@codemirror/lang-sql').then(({sql}) => sql())},
42
+ {
43
+ name: 'mysql',
44
+ loader: () => import('@codemirror/lang-sql').then(({sql, MySQL}) => sql({dialect: MySQL})),
45
+ },
46
+ {name: 'json', loader: () => import('@codemirror/lang-json').then(({json}) => json())},
47
+ {
48
+ name: 'markdown',
49
+ loader: () => import('@codemirror/lang-markdown').then(({markdown}) => markdown()),
50
+ },
51
+ {name: 'java', loader: () => import('@codemirror/lang-java').then(({java}) => java())},
52
+ {name: 'html', loader: () => import('@codemirror/lang-html').then(({html}) => html())},
53
+ {
54
+ name: 'csharp',
55
+ loader: () =>
56
+ import('@codemirror/legacy-modes/mode/clike').then(({csharp}) =>
57
+ StreamLanguage.define(csharp)
58
+ ),
59
+ },
60
+ {
61
+ name: 'sh',
62
+ loader: () =>
63
+ import('@codemirror/legacy-modes/mode/shell').then(({shell}) => StreamLanguage.define(shell)),
64
+ },
65
+ {
66
+ name: 'css',
67
+ loader: () =>
68
+ import('@codemirror/legacy-modes/mode/css').then(({css}) => StreamLanguage.define(css)),
69
+ },
70
+ {
71
+ name: 'scss',
72
+ loader: () =>
73
+ import('@codemirror/legacy-modes/mode/css').then(({css}) => StreamLanguage.define(css)),
74
+ },
75
+ {
76
+ name: 'sass',
77
+ loader: () =>
78
+ import('@codemirror/legacy-modes/mode/sass').then(({sass}) => StreamLanguage.define(sass)),
79
+ },
80
+ {
81
+ name: 'ruby',
82
+ loader: () =>
83
+ import('@codemirror/legacy-modes/mode/ruby').then(({ruby}) => StreamLanguage.define(ruby)),
84
+ },
85
+ {
86
+ name: 'python',
87
+ loader: () =>
88
+ import('@codemirror/legacy-modes/mode/python').then(({python}) =>
89
+ StreamLanguage.define(python)
90
+ ),
91
+ },
92
+ {
93
+ name: 'xml',
94
+ loader: () =>
95
+ import('@codemirror/legacy-modes/mode/xml').then(({xml}) => StreamLanguage.define(xml)),
96
+ },
97
+ {
98
+ name: 'yaml',
99
+ loader: () =>
100
+ import('@codemirror/legacy-modes/mode/yaml').then(({yaml}) => StreamLanguage.define(yaml)),
101
+ },
102
+ {
103
+ name: 'golang',
104
+ loader: () =>
105
+ import('@codemirror/legacy-modes/mode/go').then(({go}) => StreamLanguage.define(go)),
106
+ },
107
+ {name: 'text', loader: () => undefined},
108
+ {name: 'batch', loader: () => undefined},
109
+ ]
@@ -0,0 +1,164 @@
1
+ /* eslint-disable no-param-reassign */
2
+
3
+ import {Extension, StateEffect, StateField} from '@codemirror/state'
4
+ import {Decoration, EditorView, lineNumbers} from '@codemirror/view'
5
+ import {ThemeContextValue, rgba} from '@sanity/ui'
6
+
7
+ const highlightLineClass = 'cm-highlight-line'
8
+
9
+ export const addLineHighlight = StateEffect.define<number>()
10
+ export const removeLineHighlight = StateEffect.define<number>()
11
+
12
+ export const lineHighlightField = StateField.define({
13
+ create() {
14
+ return Decoration.none
15
+ },
16
+ update(lines, tr) {
17
+ lines = lines.map(tr.changes)
18
+ for (const e of tr.effects) {
19
+ if (e.is(addLineHighlight)) {
20
+ lines = lines.update({add: [lineHighlightMark.range(e.value)]})
21
+ }
22
+ if (e.is(removeLineHighlight)) {
23
+ lines = lines.update({
24
+ filter: (from) => {
25
+ // removeLineHighlight value is lineStart for the highlight, so keep other effects
26
+ return from !== e.value
27
+ },
28
+ })
29
+ }
30
+ }
31
+ return lines
32
+ },
33
+ toJSON(value, state) {
34
+ const highlightLines: number[] = []
35
+ const iter = value.iter()
36
+ while (iter.value) {
37
+ const lineNumber = state.doc.lineAt(iter.from).number
38
+ if (!highlightLines.includes(lineNumber)) {
39
+ highlightLines.push(lineNumber)
40
+ }
41
+ iter.next()
42
+ }
43
+ return highlightLines
44
+ },
45
+ fromJSON(value: number[], state) {
46
+ const lines = state.doc.lines
47
+ const highlights = value
48
+ .filter((line) => line <= lines) // one-indexed
49
+ .map((line) => lineHighlightMark.range(state.doc.line(line).from))
50
+ return Decoration.none.update({
51
+ add: highlights,
52
+ })
53
+ },
54
+ provide: (f) => EditorView.decorations.from(f),
55
+ })
56
+
57
+ const lineHighlightMark = Decoration.line({
58
+ class: highlightLineClass,
59
+ })
60
+
61
+ export const highlightState: {
62
+ [prop: string]: StateField<any>
63
+ } = {
64
+ highlight: lineHighlightField,
65
+ }
66
+
67
+ export interface HighlightLineConfig {
68
+ onHighlightChange?: (lines: number[]) => void
69
+ readOnly?: boolean
70
+ theme: ThemeContextValue
71
+ }
72
+
73
+ function createCodeMirrorTheme(options: {themeCtx: ThemeContextValue}) {
74
+ const {themeCtx} = options
75
+ const dark = {color: themeCtx.theme.color.dark[themeCtx.tone]}
76
+ const light = {color: themeCtx.theme.color.light[themeCtx.tone]}
77
+
78
+ return EditorView.baseTheme({
79
+ '.cm-lineNumbers': {
80
+ cursor: 'default',
81
+ },
82
+ '.cm-line.cm-line': {
83
+ position: 'relative',
84
+ },
85
+
86
+ // need set background with pseudoelement so it does not render over selection color
87
+ [`.${highlightLineClass}::before`]: {
88
+ position: 'absolute',
89
+ top: 0,
90
+ bottom: 0,
91
+ left: 0,
92
+ right: 0,
93
+ zIndex: -3,
94
+ content: "''",
95
+ boxSizing: 'border-box',
96
+ },
97
+ [`&dark .${highlightLineClass}::before`]: {
98
+ background: rgba(dark.color.muted.caution.pressed.bg, 0.5),
99
+ },
100
+ [`&light .${highlightLineClass}::before`]: {
101
+ background: rgba(light.color.muted.caution.pressed.bg, 0.75),
102
+ },
103
+ })
104
+ }
105
+
106
+ export const highlightLine = (config: HighlightLineConfig): Extension => {
107
+ const highlightTheme = createCodeMirrorTheme({themeCtx: config.theme})
108
+
109
+ return [
110
+ lineHighlightField,
111
+ config.readOnly
112
+ ? []
113
+ : lineNumbers({
114
+ domEventHandlers: {
115
+ mousedown: (editorView, lineInfo) => {
116
+ // Determine if the line for the clicked gutter line number has highlighted state or not
117
+ const line = editorView.state.doc.lineAt(lineInfo.from)
118
+ let isHighlighted = false
119
+ editorView.state
120
+ .field(lineHighlightField)
121
+ .between(line.from, line.to, (from, to, value) => {
122
+ if (value) {
123
+ isHighlighted = true
124
+ return false // stop iteration
125
+ }
126
+ return undefined
127
+ })
128
+
129
+ if (isHighlighted) {
130
+ editorView.dispatch({effects: removeLineHighlight.of(line.from)})
131
+ } else {
132
+ editorView.dispatch({effects: addLineHighlight.of(line.from)})
133
+ }
134
+ if (config?.onHighlightChange) {
135
+ config.onHighlightChange(editorView.state.toJSON(highlightState).highlight)
136
+ }
137
+ return true
138
+ },
139
+ },
140
+ }),
141
+ highlightTheme,
142
+ ]
143
+ }
144
+
145
+ /**
146
+ * Adds and removes highlights to the provided view using highlightLines
147
+ * @param view
148
+ * @param highlightLines
149
+ */
150
+ export function setHighlightedLines(view: EditorView, highlightLines: number[]): void {
151
+ const doc = view.state.doc
152
+ const lines = doc.lines
153
+ //1-based line numbers
154
+ const allLineNumbers = Array.from({length: lines}, (x, i) => i + 1)
155
+ view.dispatch({
156
+ effects: allLineNumbers.map((lineNumber) => {
157
+ const line = doc.line(lineNumber)
158
+ if (highlightLines?.includes(lineNumber)) {
159
+ return addLineHighlight.of(line.from)
160
+ }
161
+ return removeLineHighlight.of(line.from)
162
+ }),
163
+ })
164
+ }
@@ -0,0 +1,61 @@
1
+ import {Extension} from '@codemirror/state'
2
+ import {EditorView} from '@codemirror/view'
3
+ import {rgba, useRootTheme} from '@sanity/ui'
4
+ import {useMemo} from 'react'
5
+
6
+ export function useThemeExtension(): Extension {
7
+ const themeCtx = useRootTheme()
8
+
9
+ return useMemo(() => {
10
+ const dark = {color: themeCtx.theme.color.dark[themeCtx.tone]}
11
+ const light = {color: themeCtx.theme.color.light[themeCtx.tone]}
12
+
13
+ return EditorView.baseTheme({
14
+ '&.cm-editor': {
15
+ height: '100%',
16
+ },
17
+ '&.cm-editor.cm-focused': {
18
+ outline: 'none',
19
+ },
20
+
21
+ // Matching brackets
22
+ '&.cm-editor.cm-focused .cm-matchingBracket': {
23
+ backgroundColor: 'transparent',
24
+ },
25
+ '&.cm-editor.cm-focused .cm-nonmatchingBracket': {
26
+ backgroundColor: 'transparent',
27
+ },
28
+ '&dark.cm-editor.cm-focused .cm-matchingBracket': {
29
+ outline: `1px solid ${dark.color.base.border}`,
30
+ },
31
+ '&dark.cm-editor.cm-focused .cm-nonmatchingBracket': {
32
+ outline: `1px solid ${dark.color.base.border}`,
33
+ },
34
+ '&light.cm-editor.cm-focused .cm-matchingBracket': {
35
+ outline: `1px solid ${light.color.base.border}`,
36
+ },
37
+ '&light.cm-editor.cm-focused .cm-nonmatchingBracket': {
38
+ outline: `1px solid ${light.color.base.border}`,
39
+ },
40
+
41
+ // Size and padding of gutter
42
+ '& .cm-lineNumbers .cm-gutterElement': {
43
+ minWidth: `32px !important`,
44
+ padding: `0 8px !important`,
45
+ },
46
+ '& .cm-gutter.cm-foldGutter': {
47
+ width: `0px !important`,
48
+ },
49
+
50
+ // Color of gutter
51
+ '&dark .cm-gutters': {
52
+ color: `${rgba(dark.color.card.enabled.code.fg, 0.5)} !important`,
53
+ borderRight: `1px solid ${rgba(dark.color.base.border, 0.5)}`,
54
+ },
55
+ '&light .cm-gutters': {
56
+ color: `${rgba(light.color.card.enabled.code.fg, 0.5)} !important`,
57
+ borderRight: `1px solid ${rgba(light.color.base.border, 0.5)}`,
58
+ },
59
+ })
60
+ }, [themeCtx])
61
+ }
@@ -0,0 +1,63 @@
1
+ import {rgba, useTheme} from '@sanity/ui'
2
+ import {useMemo} from 'react'
3
+ import {createTheme} from '@uiw/codemirror-themes'
4
+ import {tags as t} from '@lezer/highlight'
5
+ import {Extension} from '@codemirror/state'
6
+
7
+ export function useCodeMirrorTheme(): Extension {
8
+ const theme = useTheme()
9
+
10
+ return useMemo(() => {
11
+ const {code: codeFont} = theme.sanity.fonts
12
+ const {base, card, dark, syntax} = theme.sanity.color
13
+
14
+ return createTheme({
15
+ theme: dark ? 'dark' : 'light',
16
+ settings: {
17
+ background: card.enabled.bg,
18
+ foreground: card.enabled.code.fg,
19
+ lineHighlight: card.enabled.bg,
20
+ fontFamily: codeFont.family,
21
+ caret: base.focusRing,
22
+ selection: rgba(base.focusRing, 0.2),
23
+ selectionMatch: rgba(base.focusRing, 0.4),
24
+ gutterBackground: card.disabled.bg,
25
+ gutterForeground: card.disabled.code.fg,
26
+ gutterActiveForeground: card.enabled.fg,
27
+ },
28
+ styles: [
29
+ {
30
+ tag: [t.heading, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6],
31
+ color: card.enabled.fg,
32
+ },
33
+ {tag: t.angleBracket, color: card.enabled.code.fg},
34
+ {tag: t.atom, color: syntax.keyword},
35
+ {tag: t.attributeName, color: syntax.attrName},
36
+ {tag: t.bool, color: syntax.boolean},
37
+ {tag: t.bracket, color: card.enabled.code.fg},
38
+ {tag: t.className, color: syntax.className},
39
+ {tag: t.comment, color: syntax.comment},
40
+ {tag: t.definition(t.typeName), color: syntax.function},
41
+ {
42
+ tag: [
43
+ t.definition(t.variableName),
44
+ t.function(t.variableName),
45
+ t.className,
46
+ t.attributeName,
47
+ ],
48
+ color: syntax.function,
49
+ },
50
+ {tag: [t.function(t.propertyName), t.propertyName], color: syntax.function},
51
+ {tag: t.keyword, color: syntax.keyword},
52
+ {tag: t.null, color: syntax.number},
53
+ {tag: t.number, color: syntax.number},
54
+ {tag: t.meta, color: card.enabled.code.fg},
55
+ {tag: t.operator, color: syntax.operator},
56
+ {tag: t.propertyName, color: syntax.property},
57
+ {tag: [t.string, t.special(t.brace)], color: syntax.string},
58
+ {tag: t.tagName, color: syntax.className},
59
+ {tag: t.typeName, color: syntax.keyword},
60
+ ],
61
+ })
62
+ }, [theme])
63
+ }
@@ -0,0 +1,24 @@
1
+ import {Extension} from '@codemirror/state'
2
+ import {EditorView} from '@codemirror/view'
3
+ import {rem, useTheme} from '@sanity/ui'
4
+ import {useMemo} from 'react'
5
+
6
+ export function useFontSizeExtension(props: {fontSize: number}): Extension {
7
+ const {fontSize: fontSizeProp} = props
8
+ const theme = useTheme()
9
+
10
+ return useMemo(() => {
11
+ const {code: codeFont} = theme.sanity.fonts
12
+ const {fontSize, lineHeight} = codeFont.sizes[fontSizeProp] || codeFont.sizes[2]
13
+
14
+ return EditorView.baseTheme({
15
+ '&': {
16
+ fontSize: rem(fontSize),
17
+ },
18
+
19
+ '& .cm-scroller': {
20
+ lineHeight: `${lineHeight / fontSize} !important`,
21
+ },
22
+ })
23
+ }, [fontSizeProp, theme])
24
+ }
@@ -0,0 +1,49 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import {Suspense} from 'react'
4
+
5
+ import {act, render} from '@testing-library/react'
6
+ import {useCodeMirror} from './useCodeMirror'
7
+ import {studioTheme, ThemeProvider} from '@sanity/ui'
8
+
9
+ describe('useCodeMirror - client', () => {
10
+ beforeEach(() => {
11
+ jest
12
+ .spyOn(window, 'requestAnimationFrame')
13
+ .mockImplementation((callback: FrameRequestCallback): number => {
14
+ try {
15
+ // eslint-disable-next-line callback-return
16
+ callback(0)
17
+ } catch (e) {
18
+ // CodeMirror does some mesurement shenanigance that json dont support
19
+ // we just let it crash silently
20
+ }
21
+ return 0
22
+ })
23
+ })
24
+
25
+ afterEach(() => {
26
+ ;(window.requestAnimationFrame as any).mockRestore()
27
+ })
28
+
29
+ it('should render suspended ace editor', async () => {
30
+ const TestComponent = () => {
31
+ const CodeMirror = useCodeMirror()
32
+ return (
33
+ <Suspense fallback={'loading'}>
34
+ {CodeMirror && (
35
+ <ThemeProvider theme={studioTheme}>
36
+ <CodeMirror languageMode={'tsx'} />
37
+ </ThemeProvider>
38
+ )}
39
+ </Suspense>
40
+ )
41
+ }
42
+ let container: any
43
+ await act(async () => {
44
+ const result = render(<TestComponent />)
45
+ container = result.container
46
+ })
47
+ expect(container.querySelector('.cm-theme')).toBeTruthy()
48
+ })
49
+ })