Skip to content

Commit a48c6b6

Browse files
committedJun 7, 2023
fix: fix view update bug. (#520)
1 parent ec18778 commit a48c6b6

File tree

5 files changed

+106
-133
lines changed

5 files changed

+106
-133
lines changed
 

‎merge/src/Internal.tsx

+59-39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useImperativeHandle, useRef } from 'react';
2-
import { EditorStateConfig } from '@codemirror/state';
2+
import { EditorStateConfig, StateEffect } from '@codemirror/state';
33
import { getDefaultExtensions } from '@uiw/react-codemirror';
44
import { MergeView, MergeConfig, DirectMergeConfig } from '@codemirror/merge';
55
import { useStore } from './store';
@@ -25,15 +25,16 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
2525
renderRevertControl,
2626
...elmProps
2727
} = props;
28-
const { modified, modifiedExtension, original, originalExtension, theme, view, dispatch, ...otherStore } = useStore();
28+
const { modified, modifiedExtension, original, originalExtension, theme, dispatch, ...otherStore } = useStore();
2929
const editor = useRef<HTMLDivElement>(null);
30+
const view = useRef<MergeView>();
3031
const opts = { orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl };
3132

3233
useImperativeHandle(
3334
ref,
3435
() => ({
3536
container: editor.current,
36-
view,
37+
view: view.current,
3738
modified,
3839
original,
3940
config: {
@@ -47,9 +48,8 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
4748
);
4849

4950
useEffect(() => {
50-
if (view && original && modified && theme && editor.current && dispatch) {
51-
editor.current.innerHTML = '';
52-
new MergeView({
51+
if (!view.current && editor.current) {
52+
view.current = new MergeView({
5353
a: {
5454
...original,
5555
extensions: [
@@ -68,24 +68,58 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
6868
...opts,
6969
});
7070
}
71-
}, [theme, editor.current, original, modified, originalExtension, modifiedExtension]);
71+
}, [view, editor]);
7272

7373
useEffect(() => {
74-
if (!view && editor.current && original?.extensions && modified?.extensions) {
75-
const viewDefault = new MergeView({
76-
a: original,
77-
b: modified,
78-
parent: editor.current,
79-
...opts,
80-
});
81-
dispatch && dispatch({ view: viewDefault, container: editor.current, ...opts });
74+
if (original && original.doc && view.current) {
75+
const originalDoc = view.current?.a.state.doc.toString();
76+
if (originalDoc !== original.doc) {
77+
view.current?.a.dispatch({
78+
changes: { from: 0, to: originalDoc.length, insert: original.doc || '' },
79+
// effects: StateEffect.reconfigure.of([
80+
// ...(originalExtension?.extension || []),
81+
// ...getDefaultExtensions({ ...originalExtension?.option, theme }),
82+
// ])
83+
});
84+
}
8285
}
83-
}, [editor.current, original, modified, view]);
86+
if (modified && modified.doc && view.current) {
87+
const modifiedDoc = view.current?.b.state.doc.toString();
88+
if (modifiedDoc !== modified.doc) {
89+
view.current?.b.dispatch({
90+
changes: { from: 0, to: modifiedDoc.length, insert: modified.doc || '' },
91+
// effects: StateEffect.reconfigure.of([
92+
// ...(modifiedExtension?.extension || []),
93+
// ...getDefaultExtensions({ ...modifiedExtension?.option, theme }),
94+
// ])
95+
});
96+
}
97+
}
98+
view.current?.destroy();
99+
view.current = new MergeView({
100+
a: {
101+
...original,
102+
extensions: [
103+
...(originalExtension?.extension || []),
104+
...getDefaultExtensions({ ...originalExtension?.option, theme }),
105+
],
106+
},
107+
b: {
108+
...modified,
109+
extensions: [
110+
...(modifiedExtension?.extension || []),
111+
...getDefaultExtensions({ ...modifiedExtension?.option, theme }),
112+
],
113+
},
114+
parent: editor.current!,
115+
...opts,
116+
});
117+
}, [view, theme, editor.current, original, modified, originalExtension, modifiedExtension]);
84118

85-
useEffect(() => () => view && view.destroy(), []);
119+
useEffect(() => () => view.current && view.current.destroy(), []);
86120

87121
useEffect(() => {
88-
if (view) {
122+
if (view.current) {
89123
const opts: MergeConfig = {};
90124
if (otherStore.orientation !== orientation) {
91125
opts.orientation = orientation;
@@ -102,29 +136,15 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
102136
if (otherStore.collapseUnchanged !== collapseUnchanged) {
103137
opts.collapseUnchanged = collapseUnchanged;
104138
}
105-
if (Object.keys(opts).length && dispatch && original && modified && editor.current) {
106-
view.destroy();
107-
const viewDefault = new MergeView({
108-
a: original,
109-
b: modified,
110-
parent: editor.current,
111-
...opts,
112-
});
113-
dispatch({ ...opts, renderRevertControl, view: viewDefault });
139+
if (otherStore.renderRevertControl !== renderRevertControl) {
140+
opts.collapseUnchanged = collapseUnchanged;
141+
}
142+
if (Object.keys(opts).length && dispatch && view.current) {
143+
view.current.reconfigure({ ...opts });
144+
dispatch({ ...opts });
114145
}
115146
}
116-
}, [
117-
view,
118-
original,
119-
modified,
120-
editor,
121-
orientation,
122-
revertControls,
123-
highlightChanges,
124-
gutter,
125-
collapseUnchanged,
126-
renderRevertControl,
127-
]);
147+
}, [dispatch, view, orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl]);
128148

129149
const defaultClassNames = 'cm-merge-theme';
130150
return (

‎merge/src/Modified.tsx

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

7-
const External = Annotation.define<boolean>();
8-
97
export interface ModifiedProps extends Omit<DefaultExtensionsOptions, 'theme'>, Omit<EditorStateConfig, 'doc'> {
108
value?: EditorStateConfig['doc'];
119
extensions?: Extension[];
@@ -15,62 +13,32 @@ export interface ModifiedProps extends Omit<DefaultExtensionsOptions, 'theme'>,
1513

1614
export const Modified = (props: ModifiedProps): JSX.Element | null => {
1715
const { extensions = [], value, selection, onChange, ...otherOption } = props;
18-
const { modified, view, theme, dispatch } = useStore();
19-
const defaultExtensionsOptions = { ...otherOption };
20-
const defaultExtensions = getDefaultExtensions({ ...defaultExtensionsOptions, theme });
16+
const { theme, dispatch } = useStore();
17+
const defaultExtensions = getDefaultExtensions({ ...otherOption, theme });
2118
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
22-
if (
23-
vu.docChanged &&
24-
typeof onChange === 'function' &&
25-
// Fix echoing of the remote changes:
26-
// If transaction is market as remote we don't have to call `onChange` handler again
27-
!vu.transactions.some((tr) => tr.annotation(External))
28-
) {
19+
if (vu.docChanged && typeof onChange === 'function') {
2920
const doc = vu.state.doc;
3021
const val = doc.toString();
3122
onChange(val, vu);
3223
}
3324
});
34-
const extensionsData = [updateListener, ...defaultExtensions, ...extensions];
35-
const data: EditorStateConfig = { extensions: [...extensionsData] };
36-
37-
useEffect(() => {
38-
dispatch!({
39-
modified: { doc: value, selection: selection, ...data },
40-
modifiedExtension: {
41-
option: defaultExtensionsOptions,
42-
extension: [updateListener, extensions],
43-
},
44-
});
45-
}, []);
4625

4726
useEffect(
4827
() =>
4928
dispatch!({
50-
modifiedExtension: { option: otherOption, extension: [updateListener, extensions] },
29+
modified: {
30+
doc: value,
31+
selection: selection,
32+
extensions: [updateListener, ...defaultExtensions, ...extensions],
33+
},
34+
modifiedExtension: {
35+
onChange,
36+
option: otherOption,
37+
extension: [updateListener, extensions],
38+
},
5139
}),
5240
[props],
5341
);
54-
55-
useEffect(() => {
56-
if (modified?.doc !== value && view) {
57-
data.doc = value;
58-
const modifiedDoc = view?.b.state.doc.toString();
59-
if (modifiedDoc !== value) {
60-
view.b.dispatch({
61-
changes: { from: 0, to: (modifiedDoc || '').length, insert: value || '' },
62-
effects: StateEffect.reconfigure.of([...extensionsData]),
63-
annotations: [External.of(true)],
64-
});
65-
}
66-
dispatch!({ modified: { ...modified, ...data } });
67-
}
68-
if (modified?.selection !== selection) {
69-
data.selection = selection;
70-
dispatch!({ modified: { ...modified, ...data } });
71-
}
72-
}, [value, extensions, selection, view]);
73-
7442
return null;
7543
};
7644

‎merge/src/Original.tsx

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

7-
const External = Annotation.define<boolean>();
8-
97
export interface OriginalProps extends Omit<DefaultExtensionsOptions, 'theme'>, Omit<EditorStateConfig, 'doc'> {
108
value?: EditorStateConfig['doc'];
119
extensions?: Extension[];
@@ -15,64 +13,33 @@ export interface OriginalProps extends Omit<DefaultExtensionsOptions, 'theme'>,
1513

1614
export const Original = (props: OriginalProps): JSX.Element | null => {
1715
const { extensions = [], value, selection, onChange, ...otherOption } = props;
18-
const { original, view, theme, dispatch } = useStore();
16+
const { theme, dispatch } = useStore();
1917
const defaultExtensions = getDefaultExtensions({ ...otherOption, theme });
2018
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
21-
if (
22-
vu.docChanged &&
23-
typeof onChange === 'function' &&
24-
// Fix echoing of the remote changes:
25-
// If transaction is market as remote we don't have to call `onChange` handler again
26-
!vu.transactions.some((tr) => tr.annotation(External))
27-
) {
19+
if (vu.docChanged && typeof onChange === 'function') {
2820
const doc = vu.state.doc;
2921
const val = doc.toString();
3022
onChange(val, vu);
3123
}
3224
});
33-
const extensionsData = [updateListener, ...defaultExtensions, ...extensions];
34-
const data: EditorStateConfig = { extensions: [...extensionsData] };
35-
36-
useEffect(() => {
37-
dispatch!({
38-
original: { doc: value, selection: selection, ...data },
39-
originalExtension: {
40-
option: otherOption,
41-
extension: [updateListener, extensions],
42-
},
43-
});
44-
}, []);
4525

4626
useEffect(
4727
() =>
4828
dispatch!({
29+
original: {
30+
doc: value,
31+
selection: selection,
32+
extensions: [updateListener, ...defaultExtensions, ...extensions],
33+
},
4934
originalExtension: {
35+
onChange,
5036
option: otherOption,
51-
extension: [updateListener, extensions],
37+
extension: [extensions, updateListener],
5238
},
5339
}),
5440
[props],
5541
);
5642

57-
useEffect(() => {
58-
if (original?.doc !== value && view) {
59-
data.doc = value;
60-
dispatch!({ original: { ...original, ...data } });
61-
const originalDoc = view?.a.state.doc.toString();
62-
if (originalDoc !== value) {
63-
view?.a.dispatch({
64-
changes: { from: 0, to: (originalDoc || '').length, insert: value || '' },
65-
effects: StateEffect.reconfigure.of([...extensionsData]),
66-
annotations: [External.of(true)],
67-
});
68-
}
69-
}
70-
if (original?.selection !== selection) {
71-
data.selection = selection;
72-
dispatch!({ original: { ...original, ...data } });
73-
}
74-
}, [value, selection, view]);
75-
7643
return null;
7744
};
7845

‎merge/src/store.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { PropsWithChildren, createContext, useContext, useEffect, useReducer } from 'react';
2-
import { EditorStateConfig } from '@codemirror/state';
2+
import { EditorStateConfig, Extension } from '@codemirror/state';
3+
import { ViewUpdate } from '@codemirror/view';
34
import { MergeView, MergeConfig } from '@codemirror/merge';
4-
import { Extension } from '@codemirror/state';
55
import { DefaultExtensionsOptions } from '@uiw/react-codemirror';
66

77
export interface StoreContextValue extends InitialState {
@@ -12,11 +12,15 @@ export interface InitialState extends MergeConfig {
1212
modifiedExtension?: {
1313
option: Omit<DefaultExtensionsOptions, 'theme'>;
1414
extension: Extension[];
15+
/** Fired whenever a change occurs to the document. */
16+
onChange?(value: string, viewUpdate: ViewUpdate): void;
1517
};
1618
modified?: EditorStateConfig;
1719
originalExtension?: {
1820
option: Omit<DefaultExtensionsOptions, 'theme'>;
1921
extension: Extension[];
22+
/** Fired whenever a change occurs to the document. */
23+
onChange?(value: string, viewUpdate: ViewUpdate): void;
2024
};
2125
original?: EditorStateConfig;
2226
view?: MergeView;

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Fragment, useState } from 'react';
1+
import { Fragment, useRef, useState } from 'react';
22
import CodeMirrorMerge, { CodeMirrorMergeProps } from 'react-codemirror-merge';
33
import { EditorView } from 'codemirror';
44
import { EditorState } from '@codemirror/state';
@@ -19,8 +19,16 @@ export const MergeExample = () => {
1919
const handleOrientation = (evn: React.ChangeEvent<HTMLSelectElement>) => {
2020
setOrientation(evn.target.value as CodeMirrorMergeProps['orientation']);
2121
};
22+
const [originalValue, setOriginalValue] = useState(originalCode);
23+
const random = useRef<number>();
24+
const click = () => {
25+
random.current = Math.floor(Math.random() * 101);
26+
const code = '// hello world' + random.current + '\n' + originalCode;
27+
setOriginalValue(code);
28+
};
2229
return (
2330
<Fragment>
31+
<button onClick={click}>Change Original Value {random.current}</button>
2432
<CodeMirrorMerge
2533
orientation={orientation}
2634
revertControls={revertControls}
@@ -30,7 +38,13 @@ export const MergeExample = () => {
3038
style={{ height: 300, overflow: 'auto' }}
3139
theme={theme}
3240
>
33-
<Original value={originalCode} extensions={[langs.javascript()]} />
41+
<Original
42+
value={originalValue}
43+
extensions={[langs.javascript()]}
44+
onChange={(val) => {
45+
// console.log('::::::::::', val)
46+
}}
47+
/>
3448
<Modified
3549
value={modifiedCode}
3650
extensions={[langs.javascript(), EditorView.editable.of(false), EditorState.readOnly.of(true)]}

0 commit comments

Comments
 (0)
Please sign in to comment.