Skip to content

Commit c608dd3

Browse files
committedJun 5, 2023
feat(codemirror-merge): add theme/ref props. (#515)
1 parent f01d52b commit c608dd3

File tree

11 files changed

+173
-63
lines changed

11 files changed

+173
-63
lines changed
 

‎core/src/getDefaultExtensions.ts

+6-12
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import { basicSetup, BasicSetupOptions } from '@uiw/codemirror-extensions-basic-
44
import { EditorView, keymap, placeholder } from '@codemirror/view';
55
import { oneDark } from '@codemirror/theme-one-dark';
66
import { EditorState } from '@codemirror/state';
7+
import { defaultLightThemeOption } from './theme/light';
78

8-
export type DefaultExtensionsOptions = {
9+
export * from '@codemirror/theme-one-dark';
10+
export * from './theme/light';
11+
12+
export interface DefaultExtensionsOptions {
913
indentWithTab?: boolean;
1014
basicSetup?: boolean | BasicSetupOptions;
1115
placeholder?: string | HTMLElement;
1216
theme?: 'light' | 'dark' | 'none' | Extension;
1317
readOnly?: boolean;
1418
editable?: boolean;
15-
};
19+
}
1620

1721
export const getDefaultExtensions = (optios: DefaultExtensionsOptions = {}): Extension[] => {
1822
const {
@@ -24,16 +28,6 @@ export const getDefaultExtensions = (optios: DefaultExtensionsOptions = {}): Ext
2428
basicSetup: defaultBasicSetup = true,
2529
} = optios;
2630
const getExtensions: Extension[] = [];
27-
const defaultLightThemeOption = EditorView.theme(
28-
{
29-
'&': {
30-
backgroundColor: '#fff',
31-
},
32-
},
33-
{
34-
dark: false,
35-
},
36-
);
3731
if (defaultIndentWithTab) {
3832
getExtensions.unshift(keymap.of([indentWithTab]));
3933
}

‎merge/README.md

+63-3
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,44 @@ export const Example = () => {
4646
};
4747
```
4848

49+
## Theme
50+
51+
```jsx
52+
import { useState } from 'react';
53+
import CodeMirrorMerge from 'react-codemirror-merge';
54+
import { EditorView } from 'codemirror';
55+
import { EditorState } from '@codemirror/state';
56+
57+
const Original = CodeMirrorMerge.Original;
58+
const Modified = CodeMirrorMerge.Modified;
59+
let doc = `one
60+
two
61+
three
62+
four
63+
five`;
64+
65+
export const Example = () => {
66+
const [theme, setTheme] = useState('light');
67+
return (
68+
<CodeMirrorMerge orientation="b-a" theme={theme}>
69+
<Original value={doc} />
70+
<Modified
71+
value={doc.replace(/t/g, 'T') + 'Six'}
72+
extensions={[EditorView.editable.of(false), EditorState.readOnly.of(true)]}
73+
/>
74+
</CodeMirrorMerge>
75+
);
76+
};
77+
```
78+
4979
## Props
5080

5181
```ts
52-
export interface CodeMirrorMergeProps extends React.HTMLAttributes<HTMLDivElement>, MergeConfig {}
82+
import { Extension } from '@codemirror/state';
83+
export interface CodeMirrorMergeRef extends InternalRef {}
84+
export interface CodeMirrorMergeProps extends React.HTMLAttributes<HTMLDivElement>, MergeConfig {
85+
theme?: 'light' | 'dark' | 'none' | Extension;
86+
}
5387

5488
interface MergeConfig {
5589
/**
@@ -92,7 +126,7 @@ interface MergeConfig {
92126
## Modified Props
93127

94128
```ts
95-
interface ModifiedProps {
129+
interface ModifiedProps extends Omit<DefaultExtensionsOptions, 'theme'> {
96130
/**
97131
The initial document. Defaults to an empty document. Can be
98132
provided either as a plain string (which will be split into
@@ -119,12 +153,25 @@ interface ModifiedProps {
119153
/** Fired whenever a change occurs to the document. */
120154
onChange?(value: string, viewUpdate: ViewUpdate): void;
121155
}
156+
157+
import { Extension } from '@codemirror/state';
158+
import { BasicSetupOptions } from '@uiw/codemirror-extensions-basic-setup';
159+
import { DefaultExtensionsOptions } from '@uiw/react-codemirror';
160+
161+
export interface DefaultExtensionsOptions {
162+
indentWithTab?: boolean;
163+
basicSetup?: boolean | BasicSetupOptions;
164+
placeholder?: string | HTMLElement;
165+
theme?: 'light' | 'dark' | 'none' | Extension;
166+
readOnly?: boolean;
167+
editable?: boolean;
168+
}
122169
```
123170

124171
## Original Props
125172

126173
```ts
127-
interface OriginalProps {
174+
interface OriginalProps extends Omit<DefaultExtensionsOptions, 'theme'> {
128175
/**
129176
The initial document. Defaults to an empty document. Can be
130177
provided either as a plain string (which will be split into
@@ -151,6 +198,19 @@ interface OriginalProps {
151198
/** Fired whenever a change occurs to the document. */
152199
onChange?(value: string, viewUpdate: ViewUpdate): void;
153200
}
201+
202+
import { Extension } from '@codemirror/state';
203+
import { BasicSetupOptions } from '@uiw/codemirror-extensions-basic-setup';
204+
import { DefaultExtensionsOptions } from '@uiw/react-codemirror';
205+
206+
export interface DefaultExtensionsOptions {
207+
indentWithTab?: boolean;
208+
basicSetup?: boolean | BasicSetupOptions;
209+
placeholder?: string | HTMLElement;
210+
theme?: 'light' | 'dark' | 'none' | Extension;
211+
readOnly?: boolean;
212+
editable?: boolean;
213+
}
154214
```
155215

156216
## Contributors

‎merge/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
"dependencies": {
3636
"@babel/runtime": "^7.18.6",
37-
"@codemirror/merge": "^6.0.1",
37+
"@codemirror/merge": "^6.1.0",
3838
"@uiw/react-codemirror": "4.20.4"
3939
},
4040
"keywords": [

‎merge/src/Internal.tsx

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import React, { useEffect, useImperativeHandle, useMemo, useRef, memo } from 'react';
2-
import { EditorStateConfig, Extension, StateEffect, Annotation } from '@codemirror/state';
3-
import { MergeView, MergeConfig } from '@codemirror/merge';
1+
import React, { useEffect, useImperativeHandle, useRef } from 'react';
2+
import { EditorStateConfig } from '@codemirror/state';
3+
import { getDefaultExtensions } from '@uiw/react-codemirror';
4+
import { MergeView, MergeConfig, DirectMergeConfig } from '@codemirror/merge';
45
import { useStore } from './store';
56
import { CodeMirrorMergeProps } from './';
67

78
export interface InternalRef {
89
container?: HTMLDivElement | null;
910
view?: MergeView;
11+
original?: EditorStateConfig;
12+
modified?: EditorStateConfig;
13+
config?: DirectMergeConfig;
1014
}
1115

12-
export const Internal = React.forwardRef((props: CodeMirrorMergeProps, ref?: React.ForwardedRef<InternalRef>) => {
16+
export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((props, ref) => {
1317
const {
1418
className,
1519
children,
@@ -21,19 +25,48 @@ export const Internal = React.forwardRef((props: CodeMirrorMergeProps, ref?: Rea
2125
renderRevertControl,
2226
...elmProps
2327
} = props;
24-
const { modified, original, view, dispatch, ...otherStore } = useStore();
28+
const { modified, modifiedExtension, original, originalExtension, theme, view, dispatch, ...otherStore } = useStore();
2529
const editor = useRef<HTMLDivElement>(null);
26-
useImperativeHandle(ref, () => ({ container: editor.current, view }), [editor, view]);
30+
const opts = { orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl };
31+
32+
useImperativeHandle(
33+
ref,
34+
() => ({
35+
container: editor.current,
36+
view,
37+
modified,
38+
original,
39+
config: {
40+
a: original!,
41+
b: modified!,
42+
parent: editor.current!,
43+
...opts,
44+
},
45+
}),
46+
[editor, view, modified, original, opts],
47+
);
48+
49+
useEffect(() => {
50+
if (view && original && modified && theme && editor.current && dispatch) {
51+
editor.current.innerHTML = '';
52+
new MergeView({
53+
a: { ...original, extensions: [...(originalExtension || []), ...getDefaultExtensions({ theme: theme })] },
54+
b: { ...modified, extensions: [...(modifiedExtension || []), ...getDefaultExtensions({ theme: theme })] },
55+
parent: editor.current,
56+
...opts,
57+
});
58+
}
59+
}, [theme, editor.current, original, modified, originalExtension, modifiedExtension]);
60+
2761
useEffect(() => {
2862
if (!view && editor.current && original?.extensions && modified?.extensions) {
29-
const opts = { orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl };
3063
const viewDefault = new MergeView({
3164
a: original,
3265
b: modified,
3366
parent: editor.current,
3467
...opts,
3568
});
36-
dispatch && dispatch({ view: viewDefault, ...opts });
69+
dispatch && dispatch({ view: viewDefault, container: editor.current, ...opts });
3770
}
3871
}, [editor.current, original, modified, view]);
3972

‎merge/src/Modified.tsx

+15-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { useEffect } from 'react';
22
import { EditorStateConfig, Extension, StateEffect, Annotation } from '@codemirror/state';
3-
import { getDefaultExtensions } from '@uiw/react-codemirror';
3+
import { getDefaultExtensions, DefaultExtensionsOptions } from '@uiw/react-codemirror';
44
import { EditorView, ViewUpdate } from '@codemirror/view';
55
import { useStore } from './store';
66

77
const External = Annotation.define<boolean>();
88

9-
export interface ModifiedProps extends Omit<EditorStateConfig, 'doc'> {
9+
export interface ModifiedProps extends Omit<DefaultExtensionsOptions, 'theme'>, Omit<EditorStateConfig, 'doc'> {
1010
value?: EditorStateConfig['doc'];
1111
extensions?: Extension[];
1212
/** Fired whenever a change occurs to the document. */
1313
onChange?(value: string, viewUpdate: ViewUpdate): void;
1414
}
1515

1616
export const Modified = (props: ModifiedProps): JSX.Element | null => {
17-
const { extensions = [], onChange } = props;
18-
const { modified, view, dispatch } = useStore();
19-
const defaultExtensions = getDefaultExtensions();
17+
const { extensions = [], selection, onChange, ...otherOption } = props;
18+
const { modified, view, theme, dispatch } = useStore();
19+
const defaultExtensions = getDefaultExtensions({ ...otherOption, theme });
2020
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
2121
if (
2222
vu.docChanged &&
@@ -34,9 +34,14 @@ export const Modified = (props: ModifiedProps): JSX.Element | null => {
3434
const data: EditorStateConfig = { extensions: [...extensionsData] };
3535

3636
useEffect(() => {
37-
dispatch!({ modified: { doc: props.value, selection: props.selection, ...data } });
37+
dispatch!({
38+
modified: { doc: props.value, selection: selection, ...data },
39+
modifiedExtension: [updateListener, extensions],
40+
});
3841
}, []);
3942

43+
useEffect(() => dispatch!({ modifiedExtension: [updateListener, extensions] }), [extensions]);
44+
4045
useEffect(() => {
4146
if (modified?.doc !== props.value && view) {
4247
data.doc = props.value;
@@ -45,16 +50,16 @@ export const Modified = (props: ModifiedProps): JSX.Element | null => {
4550
if (modifiedDoc !== props.value) {
4651
view.b.dispatch({
4752
changes: { from: 0, to: (modifiedDoc || '').length, insert: props.value || '' },
48-
effects: StateEffect.appendConfig.of([...extensionsData]),
53+
effects: StateEffect.reconfigure.of([...extensionsData]),
4954
annotations: [External.of(true)],
5055
});
5156
}
5257
}
53-
if (modified?.selection !== props.selection) {
54-
data.selection = props.selection;
58+
if (modified?.selection !== selection) {
59+
data.selection = selection;
5560
dispatch!({ modified: { ...modified, ...data } });
5661
}
57-
}, [props.value, extensions, props.selection, view]);
62+
}, [props.value, extensions, selection, view]);
5863

5964
return null;
6065
};

‎merge/src/Original.tsx

+15-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { useEffect } from 'react';
22
import { EditorStateConfig, Extension, StateEffect, Annotation } from '@codemirror/state';
33
import { EditorView, ViewUpdate } from '@codemirror/view';
4-
import { getDefaultExtensions } from '@uiw/react-codemirror';
4+
import { getDefaultExtensions, DefaultExtensionsOptions } from '@uiw/react-codemirror';
55
import { useStore } from './store';
66

77
const External = Annotation.define<boolean>();
88

9-
export interface OriginalProps extends Omit<EditorStateConfig, 'doc'> {
9+
export interface OriginalProps extends Omit<DefaultExtensionsOptions, 'theme'>, Omit<EditorStateConfig, 'doc'> {
1010
value?: EditorStateConfig['doc'];
1111
extensions?: Extension[];
1212
/** Fired whenever a change occurs to the document. */
1313
onChange?(value: string, viewUpdate: ViewUpdate): void;
1414
}
1515

1616
export const Original = (props: OriginalProps): JSX.Element | null => {
17-
const { extensions = [], onChange } = props;
18-
const { original, view, dispatch } = useStore();
19-
const defaultExtensions = getDefaultExtensions();
17+
const { extensions = [], selection, onChange, ...otherOption } = props;
18+
const { original, view, theme, dispatch } = useStore();
19+
const defaultExtensions = getDefaultExtensions({ ...otherOption, theme });
2020
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
2121
if (
2222
vu.docChanged &&
@@ -34,9 +34,14 @@ export const Original = (props: OriginalProps): JSX.Element | null => {
3434
const data: EditorStateConfig = { extensions: [...extensionsData] };
3535

3636
useEffect(() => {
37-
dispatch!({ original: { doc: props.value, selection: props.selection, ...data } });
37+
dispatch!({
38+
original: { doc: props.value, selection: selection, ...data },
39+
modifiedExtension: [updateListener, extensions],
40+
});
3841
}, []);
3942

43+
useEffect(() => dispatch!({ originalExtension: [updateListener, extensions] }), [extensions]);
44+
4045
useEffect(() => {
4146
if (original?.doc !== props.value && view) {
4247
data.doc = props.value;
@@ -45,16 +50,16 @@ export const Original = (props: OriginalProps): JSX.Element | null => {
4550
if (originalDoc !== props.value) {
4651
view?.a.dispatch({
4752
changes: { from: 0, to: (originalDoc || '').length, insert: props.value || '' },
48-
effects: StateEffect.appendConfig.of([...extensionsData]),
53+
effects: StateEffect.reconfigure.of([...extensionsData]),
4954
annotations: [External.of(true)],
5055
});
5156
}
5257
}
53-
if (original?.selection !== props.selection) {
54-
data.selection = props.selection;
58+
if (original?.selection !== selection) {
59+
data.selection = selection;
5560
dispatch!({ original: { ...original, ...data } });
5661
}
57-
}, [props.value, props.selection, view]);
62+
}, [props.value, selection, view]);
5863

5964
return null;
6065
};

‎merge/src/index.tsx

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,30 @@ import { MergeConfig } from '@codemirror/merge';
33
import { Original } from './Original';
44
import { Modified } from './Modified';
55
import { Internal, InternalRef } from './Internal';
6-
import { Provider } from './store';
6+
import { Provider, InitialState } from './store';
77

88
export interface CodeMirrorMergeRef extends InternalRef {}
9-
export interface CodeMirrorMergeProps extends React.HTMLAttributes<HTMLDivElement>, MergeConfig {}
9+
export interface CodeMirrorMergeProps extends React.HTMLAttributes<HTMLDivElement>, MergeConfig {
10+
theme?: InitialState['theme'];
11+
}
1012

11-
const InternalCodeMirror = (props: CodeMirrorMergeProps, ref?: React.ForwardedRef<InternalRef>) => {
13+
const InternalCodeMirror = React.forwardRef<CodeMirrorMergeRef, CodeMirrorMergeProps>(({ theme, ...props }, ref) => {
1214
return (
13-
<Provider>
15+
<Provider theme={theme}>
1416
<Internal {...props} ref={ref} />
1517
</Provider>
1618
);
17-
};
19+
});
1820

19-
type CodeMirrorComponent = React.FC<React.PropsWithRef<CodeMirrorMergeProps>> & {
21+
type CodeMirrorComponent = typeof InternalCodeMirror & {
2022
Original: typeof Original;
2123
Modified: typeof Modified;
2224
};
2325

24-
const ReactCodeMirrorMerge: CodeMirrorComponent = React.forwardRef<InternalRef>(
25-
InternalCodeMirror,
26-
) as unknown as CodeMirrorComponent;
26+
const CodeMirrorMerge: CodeMirrorComponent = InternalCodeMirror as unknown as CodeMirrorComponent;
2727

28-
ReactCodeMirrorMerge.Original = Original;
29-
ReactCodeMirrorMerge.Modified = Modified;
30-
ReactCodeMirrorMerge.displayName = 'CodeMirrorMerge';
28+
CodeMirrorMerge.Original = Original;
29+
CodeMirrorMerge.Modified = Modified;
30+
CodeMirrorMerge.displayName = 'CodeMirrorMerge';
3131

32-
export default ReactCodeMirrorMerge;
32+
export default CodeMirrorMerge;

‎merge/src/store.tsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import React, { PropsWithChildren, createContext, useContext, useReducer } from 'react';
1+
import React, { PropsWithChildren, createContext, useContext, useEffect, useReducer } from 'react';
22
import { EditorStateConfig } from '@codemirror/state';
33
import { MergeView, MergeConfig } from '@codemirror/merge';
4+
import { Extension } from '@codemirror/state';
45

56
export interface StoreContextValue extends InitialState {
67
dispatch?: React.Dispatch<InitialState>;
78
}
89

910
export interface InitialState extends MergeConfig {
11+
modifiedExtension?: Extension[];
1012
modified?: EditorStateConfig;
13+
originalExtension?: Extension[];
1114
original?: EditorStateConfig;
1215
view?: MergeView;
16+
theme?: 'light' | 'dark' | 'none' | Extension;
17+
container?: HTMLDivElement | null;
1318
}
1419

1520
export const initialState: InitialState = {
@@ -32,7 +37,12 @@ export const useStore = () => {
3237
return useContext(Context);
3338
};
3439

35-
export const Provider: React.FC<PropsWithChildren<any>> = ({ children }) => {
36-
const [state, dispatch] = useReducer(reducer, initialState);
40+
export interface ProviderProps {
41+
theme?: InitialState['theme'];
42+
}
43+
44+
export const Provider: React.FC<PropsWithChildren<ProviderProps>> = ({ children, theme }) => {
45+
const [state, dispatch] = useReducer(reducer, { ...initialState, theme });
46+
useEffect(() => dispatch({ theme }), [theme]);
3747
return <Context.Provider value={{ ...state, dispatch }}>{children}</Context.Provider>;
3848
};

‎themes/eclipse/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ npm install @uiw/codemirror-theme-eclipse --save
1717
```
1818

1919
```jsx
20+
import { defaultSettingsEclipse } from '@uiw/codemirror-theme-eclipse';
2021
import { eclipse, eclipseInit } from '@uiw/codemirror-theme-eclipse';
2122

2223
<CodeMirror theme={eclipse} />

‎www/src/pages/merge/Example.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import CodeMirrorMerge, { CodeMirrorMergeProps } from 'react-codemirror-merge';
33
import { EditorView } from 'codemirror';
44
import { EditorState } from '@codemirror/state';
55
import { langs } from '@uiw/codemirror-extensions-langs';
6-
76
import { originalCode, modifiedCode } from './code';
7+
import { useTheme } from '../../utils/useTheme';
88

99
const Original = CodeMirrorMerge.Original;
1010
const Modified = CodeMirrorMerge.Modified;
@@ -15,6 +15,7 @@ export const MergeExample = () => {
1515
const [highlightChanges, setHighlightChanges] = useState<CodeMirrorMergeProps['highlightChanges']>(true);
1616
const [gutter, setGutter] = useState<CodeMirrorMergeProps['gutter']>(true);
1717
const [collapseUnchanged, setCollapseUnchanged] = useState<CodeMirrorMergeProps['collapseUnchanged']>({});
18+
const { theme } = useTheme();
1819
const handleOrientation = (evn: React.ChangeEvent<HTMLSelectElement>) => {
1920
setOrientation(evn.target.value as CodeMirrorMergeProps['orientation']);
2021
};
@@ -27,6 +28,7 @@ export const MergeExample = () => {
2728
highlightChanges={highlightChanges}
2829
gutter={gutter}
2930
style={{ height: 300, overflow: 'auto' }}
31+
theme={theme}
3032
>
3133
<Original value={originalCode} extensions={[langs.javascript()]} />
3234
<Modified

‎www/src/utils/useTheme.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useEffect, useState } from 'react';
22
import { ReactCodeMirrorProps } from '@uiw/react-codemirror';
33

4-
export function useTheme() {
4+
export function useTheme(name: ReactCodeMirrorProps['theme'] = 'light') {
55
const dark = document.documentElement.getAttribute('data-color-mode');
6-
const [theme, setTheme] = useState<ReactCodeMirrorProps['theme']>(dark === 'dark' ? 'dark' : 'light');
6+
const [theme, setTheme] = useState<ReactCodeMirrorProps['theme']>(dark === 'dark' ? 'dark' : name);
77
useEffect(() => {
88
setTheme(document.documentElement.getAttribute('data-color-mode') === 'dark' ? 'dark' : 'light');
99
document.addEventListener('colorschemechange', (e) => {

0 commit comments

Comments
 (0)
Please sign in to comment.