Skip to content

Commit c608dd3

Browse files
committed
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

Lines changed: 6 additions & 12 deletions
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

Lines changed: 63 additions & 3 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 41 additions & 8 deletions
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

Lines changed: 15 additions & 10 deletions
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

Lines changed: 15 additions & 10 deletions
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
};

0 commit comments

Comments
 (0)