Skip to content

Commit ddf13f4

Browse files
committed
refac EditorView creation; CM effects props; add useExtension hook;
- streamlined EditorView creation, switched from state based to ref+memo based with optimized `useLayoutEffect` rendering - fixes flickering - fixes painting delays - cleanup unneeded checks & code - easier render-flow intergration - new `effects` prop + param (`CodeMirror`/`useCodeMirror`) to apply editor effects from e.g. extensions without having the `editor` in the actual component - new `useExtension` hook to easily update extensions in a `useCallback` fashion - fixes unnecessary editor destroy & creation (history lost, flickering etc.) - mui add new `embed` variant to `useEditorTheme` - mui add new `dense` option to `useEditorTheme` - optimized colors in theme + highlighting - now defaults to fullwidth, to be easier styleable from container component
1 parent 0603e9d commit ddf13f4

File tree

21 files changed

+531
-435
lines changed

21 files changed

+531
-435
lines changed

package-lock.json

Lines changed: 0 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/demo/package-lock.json

Lines changed: 171 additions & 184 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/demo/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { customWidgets } from './components/UISchema'
1212
const customThemes = customTheme('#05aeca')
1313

1414
export const App: React.ComponentType<{}> = () => {
15-
const [themeId] = React.useState<'dark' | 'light'>('dark')
15+
const [themeId, setThemeId] = React.useState<'dark' | 'light'>('dark')
1616

1717
const [theme, setTheme] = React.useState(customThemes[themeId])
1818
React.useEffect(() => {
@@ -26,7 +26,7 @@ export const App: React.ComponentType<{}> = () => {
2626
<BrowserRouter>
2727
<React.Suspense fallback={<CircularProgress/>}>
2828
<UIMetaProvider t={browserT} widgets={customWidgets}>
29-
<Layout/>
29+
<Layout setTheme={setThemeId}/>
3030
</UIMetaProvider>
3131
</React.Suspense>
3232
</BrowserRouter>

packages/demo/src/components/CustomCodeMirror.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,35 @@ import { history, defaultKeymap, historyKeymap, indentWithTab } from '@codemirro
1111
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
1212
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'
1313
import { lintKeymap } from '@codemirror/lint'
14-
import { Compartment, EditorState } from '@codemirror/state'
14+
import { Compartment, EditorState, Extension } from '@codemirror/state'
1515
import { useEditorTheme } from '@ui-schema/material-code/useEditorTheme'
1616
import { useHighlightStyle } from '@ui-schema/material-code/useHighlightStyle'
1717
import { CodeMirrorComponentProps, CodeMirror, CodeMirrorProps } from '@ui-schema/kit-codemirror/CodeMirror'
18+
import { useExtension } from '@ui-schema/kit-codemirror/useExtension'
1819

1920
export const CustomCodeMirror: React.FC<CodeMirrorComponentProps> = (
2021
{
2122
// values we want to override in this component
22-
value, extensions,
23+
value, extensions, effects,
2324
// everything else is just passed down
2425
...props
2526
},
2627
) => {
2728
const {onChange} = props
2829
const theme = useEditorTheme(typeof onChange === 'undefined')
2930
const highlightStyle = useHighlightStyle()
31+
const {init: initHighlightExt, effects: effectsHighlightExt} = useExtension(
32+
() => syntaxHighlighting(highlightStyle || defaultHighlightStyle, {fallback: true}),
33+
[highlightStyle],
34+
)
35+
const {init: initThemeExt, effects: effectsThemeExt} = useExtension(
36+
() => theme,
37+
[theme],
38+
)
39+
const themeCompartment = React.useRef<Compartment>(new Compartment())
40+
const effectsRef = React.useRef<((editor: EditorView) => void)[]>(effects || [])
3041

31-
const extensionsAll = React.useMemo(() => [
42+
const extensionsAll: Extension[] = React.useMemo(() => [
3243
lineNumbers(),
3344
EditorView.lineWrapping,
3445
highlightActiveLineGutter(),
@@ -38,16 +49,15 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps> = (
3849
drawSelection(),
3950
dropCursor(),
4051
EditorState.allowMultipleSelections.of(true),
52+
new Compartment().of(EditorState.tabSize.of(4)),
4153
indentOnInput(),
42-
syntaxHighlighting(highlightStyle || defaultHighlightStyle, {fallback: true}),
4354
bracketMatching(),
4455
closeBrackets(),
4556
autocompletion(),
4657
rectangularSelection(),
4758
// crosshairCursor(),
4859
highlightActiveLine(),
4960
highlightSelectionMatches(),
50-
new Compartment().of(EditorState.tabSize.of(4)),
5161
keymap.of([
5262
...closeBracketsKeymap,
5363
...defaultKeymap,
@@ -58,9 +68,31 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps> = (
5868
...lintKeymap,
5969
indentWithTab,
6070
]),
61-
theme,
71+
initHighlightExt(),
72+
initThemeExt(),
73+
// themeCompartment.current.of(themeRef.current),// only initial theme here to not re-create extensions
6274
...(extensions || []),
63-
], [highlightStyle, extensions, theme])
75+
], [extensions, initHighlightExt, initThemeExt])
76+
77+
React.useMemo(() => {
78+
if(!effects) return
79+
effectsRef.current.push(...effects)
80+
}, [effects])
81+
React.useMemo(() => {
82+
effectsRef.current.push(
83+
function updateTheme(editor) {
84+
editor.dispatch({
85+
effects: themeCompartment.current.reconfigure(theme),
86+
})
87+
},
88+
)
89+
}, [theme])
90+
React.useMemo(() => {
91+
effectsRef.current.push(...effectsHighlightExt)
92+
}, [effectsHighlightExt])
93+
React.useMemo(() => {
94+
effectsRef.current.push(...effectsThemeExt)
95+
}, [effectsThemeExt])
6496

6597
const onViewLifecycle: CodeMirrorProps['onViewLifecycle'] = React.useCallback((view) => {
6698
console.log('on-view-lifecycle', view)
@@ -70,6 +102,7 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps> = (
70102
value={value || ''}
71103
extensions={extensionsAll}
72104
onViewLifecycle={onViewLifecycle}
105+
effects={effectsRef.current.splice(0, effectsRef.current.length)}
73106
{...props}
74107
// className={className}
75108
/>

packages/demo/src/components/Layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { PageHome } from '../pages/PageHome'
44
import { PageDemoWidget } from '../pages/PageDemoWidget'
55
import { PageDemoLangSelectable } from '../pages/PageDemoLangSelectable'
66
import { PageDemoComponent } from '../pages/PageDemoComponent'
7+
import { Button } from '@mui/material'
78

8-
export const Layout: React.ComponentType<{}> = () => {
9+
export const Layout: React.ComponentType<{ setTheme: React.Dispatch<React.SetStateAction<'dark' | 'light'>> }> = ({setTheme}) => {
910
const scrollWrapper = React.useRef<HTMLDivElement | null>(null)
1011

1112
return <div
@@ -15,7 +16,6 @@ export const Layout: React.ComponentType<{}> = () => {
1516
flexDirection: 'column',
1617
maxHeight: '100%',
1718
position: 'relative',
18-
color: '#ffffff',
1919
overflow: 'auto',
2020
padding: '0 12px',
2121
}}
@@ -26,5 +26,8 @@ export const Layout: React.ComponentType<{}> = () => {
2626
<Route path={'/widget'} element={<PageDemoWidget/>}/>
2727
<Route path={'/selectable'} element={<PageDemoLangSelectable/>}/>
2828
</Routes>
29+
<Button size={'small'} onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
30+
switch theme
31+
</Button>
2932
</div>
3033
}

packages/demo/src/pages/PageDemoComponent.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Box from '@mui/material/Box'
44
import Link from '@mui/material/Link'
55
import Typography from '@mui/material/Typography'
66
import { Nav } from '../components/Nav'
7-
import { CodeMirrorOnChange } from '@ui-schema/kit-codemirror'
7+
import { CodeMirrorOnChange } from '@ui-schema/kit-codemirror/useCodeMirror'
88
import { CodeMirrorComponentProps, CodeMirror, CodeMirrorProps } from '@ui-schema/kit-codemirror/CodeMirror'
99
import {
1010
lineNumbers, highlightActiveLineGutter, highlightSpecialChars,
@@ -110,10 +110,11 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps> = (
110110
const DemoComponent = () => {
111111
const [value, setValue] = React.useState('')
112112

113-
const onChange: CodeMirrorOnChange = React.useCallback((_editor, nextValue, prevValue) => {
114-
if(nextValue !== prevValue) {
115-
setValue(nextValue)
113+
const onChange: CodeMirrorOnChange = React.useCallback((editor, newValue) => {
114+
if(!editor.docChanged || typeof newValue !== 'string') {
115+
return
116116
}
117+
setValue(newValue)
117118
}, [setValue])
118119

119120
return <React.Fragment>
@@ -128,10 +129,11 @@ const DemoComponentReadOnly = () => {
128129
const [readOnly, setReadOnly] = React.useState(false)
129130
const [value, setValue] = React.useState('')
130131

131-
const onChange: CodeMirrorOnChange = React.useCallback((_editor, nextValue, prevValue) => {
132-
if(nextValue !== prevValue) {
133-
setValue(nextValue)
132+
const onChange: CodeMirrorOnChange = React.useCallback((editor, newValue) => {
133+
if(!editor.docChanged || typeof newValue !== 'string') {
134+
return
134135
}
136+
setValue(newValue)
135137
}, [setValue])
136138

137139
return <React.Fragment>

packages/kit-codemirror/package-lock.json

Lines changed: 32 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/kit-codemirror/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ui-schema/kit-codemirror",
3-
"version": "0.0.1",
3+
"version": "0.1.0-alpha.0",
44
"description": "CodeMirror v6 as React Component, with hooks and stuff - but only the necessities.",
55
"homepage": "https://ui-schema.bemit.codes/docs/kit-codemirror/kit-codemirror",
66
"author": {

packages/kit-codemirror/src/CodeMirror/CodeMirror.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface CodeMirrorComponentProps {
99
value?: string
1010
extensions?: Extension[]
1111
classNamesContent?: string[]
12+
effects?: ((editor: EditorView) => void)[]
1213
}
1314

1415
export interface CodeMirrorProps extends CodeMirrorComponentProps {
@@ -29,6 +30,7 @@ export const CodeMirror: React.FC<CodeMirrorProps> = (
2930
value = '',
3031
style,
3132
extensions,
33+
effects,
3234
},
3335
) => {
3436
const containerRef = React.useRef<HTMLDivElement | null>(null)
@@ -40,11 +42,12 @@ export const CodeMirror: React.FC<CodeMirrorProps> = (
4042
...(extensions || []),
4143
], [extensions])
4244

43-
const [editor] = useCodeMirror(
45+
const editor = useCodeMirror(
4446
containerRef,
4547
onChange,
4648
value,
4749
extensionsAll,
50+
effects,
4851
)
4952

5053
// but extensions need to receive both: Compartment and Editor (and optionally their values)

0 commit comments

Comments
 (0)