@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.
- package/LICENSE +1 -1
- package/README.md +140 -64
- package/lib/_chunks/CodeMirrorProxy-3836f097.js +619 -0
- package/lib/_chunks/CodeMirrorProxy-3836f097.js.map +1 -0
- package/lib/_chunks/CodeMirrorProxy-e83d4d37.js +611 -0
- package/lib/_chunks/CodeMirrorProxy-e83d4d37.js.map +1 -0
- package/lib/_chunks/index-17e68aff.js +563 -0
- package/lib/_chunks/index-17e68aff.js.map +1 -0
- package/lib/_chunks/index-9a4cb814.js +549 -0
- package/lib/_chunks/index-9a4cb814.js.map +1 -0
- package/lib/{src/index.d.ts → index.d.ts} +18 -10
- package/lib/index.esm.js +1 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +10 -1
- package/lib/index.js.map +1 -1
- package/package.json +53 -27
- package/src/CodeInput.tsx +72 -272
- package/src/LanguageField.tsx +33 -0
- package/src/LanguageInput.tsx +32 -0
- package/src/PreviewCode.tsx +40 -68
- package/src/__workshop__/index.ts +22 -0
- package/src/__workshop__/lazy.tsx +54 -0
- package/src/__workshop__/preview.tsx +24 -0
- package/src/__workshop__/props.tsx +24 -0
- package/src/codemirror/CodeMirrorProxy.tsx +157 -0
- package/src/codemirror/CodeModeContext.tsx +4 -0
- package/src/codemirror/defaultCodeModes.ts +109 -0
- package/src/codemirror/extensions/highlightLineExtension.ts +164 -0
- package/src/codemirror/extensions/theme.ts +61 -0
- package/src/codemirror/extensions/useCodeMirrorTheme.ts +63 -0
- package/src/codemirror/extensions/useFontSize.ts +24 -0
- package/src/codemirror/useCodeMirror-client.test.tsx +49 -0
- package/src/{ace-editor/AceEditor-server.test.tsx → codemirror/useCodeMirror-server.test.tsx} +3 -4
- package/src/codemirror/useCodeMirror.tsx +12 -0
- package/src/codemirror/useLanguageMode.tsx +52 -0
- package/src/config.ts +1 -13
- package/src/getMedia.tsx +0 -2
- package/src/index.ts +4 -11
- package/src/plugin.tsx +39 -0
- package/src/schema.tsx +3 -7
- package/src/types.ts +19 -3
- package/src/ui/focusRingStyle.ts +27 -0
- package/src/useFieldMember.ts +16 -0
- package/lib/_chunks/editorSupport-895caf32.esm.js +0 -2
- package/lib/_chunks/editorSupport-895caf32.esm.js.map +0 -1
- package/lib/_chunks/editorSupport-bda3d360.js +0 -2
- package/lib/_chunks/editorSupport-bda3d360.js.map +0 -1
- package/src/ace-editor/AceEditor-client.test.tsx +0 -37
- package/src/ace-editor/AceEditorLazy.tsx +0 -19
- package/src/ace-editor/editorSupport.ts +0 -34
- package/src/ace-editor/groq.ts +0 -630
- 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,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
|
+
})
|