Skip to content

Commit

Permalink
fix: fix view update bug. (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaywcjlove committed Jun 7, 2023
1 parent ec18778 commit a48c6b6
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 133 deletions.
98 changes: 59 additions & 39 deletions merge/src/Internal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useImperativeHandle, useRef } from 'react';
import { EditorStateConfig } from '@codemirror/state';
import { EditorStateConfig, StateEffect } from '@codemirror/state';
import { getDefaultExtensions } from '@uiw/react-codemirror';
import { MergeView, MergeConfig, DirectMergeConfig } from '@codemirror/merge';
import { useStore } from './store';
Expand All @@ -25,15 +25,16 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
renderRevertControl,
...elmProps
} = props;
const { modified, modifiedExtension, original, originalExtension, theme, view, dispatch, ...otherStore } = useStore();
const { modified, modifiedExtension, original, originalExtension, theme, dispatch, ...otherStore } = useStore();
const editor = useRef<HTMLDivElement>(null);
const view = useRef<MergeView>();
const opts = { orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl };

useImperativeHandle(
ref,
() => ({
container: editor.current,
view,
view: view.current,
modified,
original,
config: {
Expand All @@ -47,9 +48,8 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
);

useEffect(() => {
if (view && original && modified && theme && editor.current && dispatch) {
editor.current.innerHTML = '';
new MergeView({
if (!view.current && editor.current) {
view.current = new MergeView({
a: {
...original,
extensions: [
Expand All @@ -68,24 +68,58 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
...opts,
});
}
}, [theme, editor.current, original, modified, originalExtension, modifiedExtension]);
}, [view, editor]);

useEffect(() => {
if (!view && editor.current && original?.extensions && modified?.extensions) {
const viewDefault = new MergeView({
a: original,
b: modified,
parent: editor.current,
...opts,
});
dispatch && dispatch({ view: viewDefault, container: editor.current, ...opts });
if (original && original.doc && view.current) {
const originalDoc = view.current?.a.state.doc.toString();
if (originalDoc !== original.doc) {
view.current?.a.dispatch({
changes: { from: 0, to: originalDoc.length, insert: original.doc || '' },
// effects: StateEffect.reconfigure.of([
// ...(originalExtension?.extension || []),
// ...getDefaultExtensions({ ...originalExtension?.option, theme }),
// ])
});
}
}
}, [editor.current, original, modified, view]);
if (modified && modified.doc && view.current) {
const modifiedDoc = view.current?.b.state.doc.toString();
if (modifiedDoc !== modified.doc) {
view.current?.b.dispatch({
changes: { from: 0, to: modifiedDoc.length, insert: modified.doc || '' },
// effects: StateEffect.reconfigure.of([
// ...(modifiedExtension?.extension || []),
// ...getDefaultExtensions({ ...modifiedExtension?.option, theme }),
// ])
});
}
}
view.current?.destroy();
view.current = new MergeView({
a: {
...original,
extensions: [
...(originalExtension?.extension || []),
...getDefaultExtensions({ ...originalExtension?.option, theme }),
],
},
b: {
...modified,
extensions: [
...(modifiedExtension?.extension || []),
...getDefaultExtensions({ ...modifiedExtension?.option, theme }),
],
},
parent: editor.current!,
...opts,
});
}, [view, theme, editor.current, original, modified, originalExtension, modifiedExtension]);

useEffect(() => () => view && view.destroy(), []);
useEffect(() => () => view.current && view.current.destroy(), []);

useEffect(() => {
if (view) {
if (view.current) {
const opts: MergeConfig = {};
if (otherStore.orientation !== orientation) {
opts.orientation = orientation;
Expand All @@ -102,29 +136,15 @@ export const Internal = React.forwardRef<InternalRef, CodeMirrorMergeProps>((pro
if (otherStore.collapseUnchanged !== collapseUnchanged) {
opts.collapseUnchanged = collapseUnchanged;
}
if (Object.keys(opts).length && dispatch && original && modified && editor.current) {
view.destroy();
const viewDefault = new MergeView({
a: original,
b: modified,
parent: editor.current,
...opts,
});
dispatch({ ...opts, renderRevertControl, view: viewDefault });
if (otherStore.renderRevertControl !== renderRevertControl) {
opts.collapseUnchanged = collapseUnchanged;
}
if (Object.keys(opts).length && dispatch && view.current) {
view.current.reconfigure({ ...opts });
dispatch({ ...opts });
}
}
}, [
view,
original,
modified,
editor,
orientation,
revertControls,
highlightChanges,
gutter,
collapseUnchanged,
renderRevertControl,
]);
}, [dispatch, view, orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl]);

const defaultClassNames = 'cm-merge-theme';
return (
Expand Down
60 changes: 14 additions & 46 deletions merge/src/Modified.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { EditorStateConfig, Extension, StateEffect, Annotation } from '@codemirror/state';
import { getDefaultExtensions, DefaultExtensionsOptions } from '@uiw/react-codemirror';
import { EditorStateConfig, Extension } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
import { useStore } from './store';

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

export interface ModifiedProps extends Omit<DefaultExtensionsOptions, 'theme'>, Omit<EditorStateConfig, 'doc'> {
value?: EditorStateConfig['doc'];
extensions?: Extension[];
Expand All @@ -15,62 +13,32 @@ export interface ModifiedProps extends Omit<DefaultExtensionsOptions, 'theme'>,

export const Modified = (props: ModifiedProps): JSX.Element | null => {
const { extensions = [], value, selection, onChange, ...otherOption } = props;
const { modified, view, theme, dispatch } = useStore();
const defaultExtensionsOptions = { ...otherOption };
const defaultExtensions = getDefaultExtensions({ ...defaultExtensionsOptions, theme });
const { theme, dispatch } = useStore();
const defaultExtensions = getDefaultExtensions({ ...otherOption, theme });
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
if (
vu.docChanged &&
typeof onChange === 'function' &&
// Fix echoing of the remote changes:
// If transaction is market as remote we don't have to call `onChange` handler again
!vu.transactions.some((tr) => tr.annotation(External))
) {
if (vu.docChanged && typeof onChange === 'function') {
const doc = vu.state.doc;
const val = doc.toString();
onChange(val, vu);
}
});
const extensionsData = [updateListener, ...defaultExtensions, ...extensions];
const data: EditorStateConfig = { extensions: [...extensionsData] };

useEffect(() => {
dispatch!({
modified: { doc: value, selection: selection, ...data },
modifiedExtension: {
option: defaultExtensionsOptions,
extension: [updateListener, extensions],
},
});
}, []);

useEffect(
() =>
dispatch!({
modifiedExtension: { option: otherOption, extension: [updateListener, extensions] },
modified: {
doc: value,
selection: selection,
extensions: [updateListener, ...defaultExtensions, ...extensions],
},
modifiedExtension: {
onChange,
option: otherOption,
extension: [updateListener, extensions],
},
}),
[props],
);

useEffect(() => {
if (modified?.doc !== value && view) {
data.doc = value;
const modifiedDoc = view?.b.state.doc.toString();
if (modifiedDoc !== value) {
view.b.dispatch({
changes: { from: 0, to: (modifiedDoc || '').length, insert: value || '' },
effects: StateEffect.reconfigure.of([...extensionsData]),
annotations: [External.of(true)],
});
}
dispatch!({ modified: { ...modified, ...data } });
}
if (modified?.selection !== selection) {
data.selection = selection;
dispatch!({ modified: { ...modified, ...data } });
}
}, [value, extensions, selection, view]);

return null;
};

Expand Down
55 changes: 11 additions & 44 deletions merge/src/Original.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import { EditorStateConfig, Extension, StateEffect, Annotation } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
import { getDefaultExtensions, DefaultExtensionsOptions } from '@uiw/react-codemirror';
import { EditorStateConfig, Extension } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
import { useStore } from './store';

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

export interface OriginalProps extends Omit<DefaultExtensionsOptions, 'theme'>, Omit<EditorStateConfig, 'doc'> {
value?: EditorStateConfig['doc'];
extensions?: Extension[];
Expand All @@ -15,64 +13,33 @@ export interface OriginalProps extends Omit<DefaultExtensionsOptions, 'theme'>,

export const Original = (props: OriginalProps): JSX.Element | null => {
const { extensions = [], value, selection, onChange, ...otherOption } = props;
const { original, view, theme, dispatch } = useStore();
const { theme, dispatch } = useStore();
const defaultExtensions = getDefaultExtensions({ ...otherOption, theme });
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
if (
vu.docChanged &&
typeof onChange === 'function' &&
// Fix echoing of the remote changes:
// If transaction is market as remote we don't have to call `onChange` handler again
!vu.transactions.some((tr) => tr.annotation(External))
) {
if (vu.docChanged && typeof onChange === 'function') {
const doc = vu.state.doc;
const val = doc.toString();
onChange(val, vu);
}
});
const extensionsData = [updateListener, ...defaultExtensions, ...extensions];
const data: EditorStateConfig = { extensions: [...extensionsData] };

useEffect(() => {
dispatch!({
original: { doc: value, selection: selection, ...data },
originalExtension: {
option: otherOption,
extension: [updateListener, extensions],
},
});
}, []);

useEffect(
() =>
dispatch!({
original: {
doc: value,
selection: selection,
extensions: [updateListener, ...defaultExtensions, ...extensions],
},
originalExtension: {
onChange,
option: otherOption,
extension: [updateListener, extensions],
extension: [extensions, updateListener],
},
}),
[props],
);

useEffect(() => {
if (original?.doc !== value && view) {
data.doc = value;
dispatch!({ original: { ...original, ...data } });
const originalDoc = view?.a.state.doc.toString();
if (originalDoc !== value) {
view?.a.dispatch({
changes: { from: 0, to: (originalDoc || '').length, insert: value || '' },
effects: StateEffect.reconfigure.of([...extensionsData]),
annotations: [External.of(true)],
});
}
}
if (original?.selection !== selection) {
data.selection = selection;
dispatch!({ original: { ...original, ...data } });
}
}, [value, selection, view]);

return null;
};

Expand Down
8 changes: 6 additions & 2 deletions merge/src/store.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { PropsWithChildren, createContext, useContext, useEffect, useReducer } from 'react';
import { EditorStateConfig } from '@codemirror/state';
import { EditorStateConfig, Extension } from '@codemirror/state';
import { ViewUpdate } from '@codemirror/view';
import { MergeView, MergeConfig } from '@codemirror/merge';
import { Extension } from '@codemirror/state';
import { DefaultExtensionsOptions } from '@uiw/react-codemirror';

export interface StoreContextValue extends InitialState {
Expand All @@ -12,11 +12,15 @@ export interface InitialState extends MergeConfig {
modifiedExtension?: {
option: Omit<DefaultExtensionsOptions, 'theme'>;
extension: Extension[];
/** Fired whenever a change occurs to the document. */
onChange?(value: string, viewUpdate: ViewUpdate): void;
};
modified?: EditorStateConfig;
originalExtension?: {
option: Omit<DefaultExtensionsOptions, 'theme'>;
extension: Extension[];
/** Fired whenever a change occurs to the document. */
onChange?(value: string, viewUpdate: ViewUpdate): void;
};
original?: EditorStateConfig;
view?: MergeView;
Expand Down
18 changes: 16 additions & 2 deletions www/src/pages/merge/Example.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment, useState } from 'react';
import { Fragment, useRef, useState } from 'react';
import CodeMirrorMerge, { CodeMirrorMergeProps } from 'react-codemirror-merge';
import { EditorView } from 'codemirror';
import { EditorState } from '@codemirror/state';
Expand All @@ -19,8 +19,16 @@ export const MergeExample = () => {
const handleOrientation = (evn: React.ChangeEvent<HTMLSelectElement>) => {
setOrientation(evn.target.value as CodeMirrorMergeProps['orientation']);
};
const [originalValue, setOriginalValue] = useState(originalCode);
const random = useRef<number>();
const click = () => {
random.current = Math.floor(Math.random() * 101);
const code = '// hello world' + random.current + '\n' + originalCode;
setOriginalValue(code);
};
return (
<Fragment>
<button onClick={click}>Change Original Value {random.current}</button>
<CodeMirrorMerge
orientation={orientation}
revertControls={revertControls}
Expand All @@ -30,7 +38,13 @@ export const MergeExample = () => {
style={{ height: 300, overflow: 'auto' }}
theme={theme}
>
<Original value={originalCode} extensions={[langs.javascript()]} />
<Original
value={originalValue}
extensions={[langs.javascript()]}
onChange={(val) => {
// console.log('::::::::::', val)
}}
/>
<Modified
value={modifiedCode}
extensions={[langs.javascript(), EditorView.editable.of(false), EditorState.readOnly.of(true)]}
Expand Down

0 comments on commit a48c6b6

Please sign in to comment.