Skip to content

Commit 3198a3c

Browse files
committed
feat(CodeMirrorMerge): add orientation, revertControls, highlightChanges, gutter props.
1 parent d7ca858 commit 3198a3c

File tree

9 files changed

+324
-48
lines changed

9 files changed

+324
-48
lines changed

merge/README.md

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ five`;
3535

3636
export const Example = () => {
3737
return (
38-
<CodeMirrorMerge>
38+
<CodeMirrorMerge orientation="b-a">
3939
<Original value={doc} />
4040
<Modified
4141
value={doc.replace(/t/g, 'T') + 'Six'}
@@ -46,6 +46,109 @@ export const Example = () => {
4646
};
4747
```
4848

49+
## Props
50+
51+
```ts
52+
export interface CodeMirrorMergeProps extends React.HTMLAttributes<HTMLDivElement>, MergeConfig {}
53+
54+
interface MergeConfig {
55+
/**
56+
Controls whether editor A or editor B is shown first. Defaults
57+
to `"a-b"`.
58+
*/
59+
orientation?: 'a-b' | 'b-a';
60+
/**
61+
Controls whether revert controls are shown between changed
62+
chunks.
63+
*/
64+
revertControls?: 'a-to-b' | 'b-to-a';
65+
/**
66+
When given, this function is called to render the button to
67+
revert a chunk.
68+
*/
69+
renderRevertControl?: () => HTMLElement;
70+
/**
71+
By default, the merge view will mark inserted and deleted text
72+
in changed chunks. Set this to false to turn that off.
73+
*/
74+
highlightChanges?: boolean;
75+
/**
76+
Controls whether a gutter marker is shown next to changed lines.
77+
*/
78+
gutter?: boolean;
79+
/**
80+
When given, long stretches of unchanged text are collapsed.
81+
`margin` gives the number of lines to leave visible after/before
82+
a change (default is 3), and `minSize` gives the minimum amount
83+
of collapsible lines that need to be present (defaults to 4).
84+
*/
85+
collapseUnchanged?: {
86+
margin?: number;
87+
minSize?: number;
88+
};
89+
}
90+
```
91+
92+
## Modified Props
93+
94+
```ts
95+
interface ModifiedProps {
96+
/**
97+
The initial document. Defaults to an empty document. Can be
98+
provided either as a plain string (which will be split into
99+
lines according to the value of the [`lineSeparator`
100+
facet](https://codemirror.net/6/docs/ref/#state.EditorState^lineSeparator)), or an instance of
101+
the [`Text`](https://codemirror.net/6/docs/ref/#state.Text) class (which is what the state will use
102+
to represent the document).
103+
*/
104+
value?: string | Text;
105+
/**
106+
The starting selection. Defaults to a cursor at the very start
107+
of the document.
108+
*/
109+
selection?:
110+
| EditorSelection
111+
| {
112+
anchor: number;
113+
head?: number;
114+
};
115+
/**
116+
[Extension(s)](https://codemirror.net/6/docs/ref/#state.Extension) to associate with this state.
117+
*/
118+
extensions?: Extension;
119+
}
120+
```
121+
122+
## Original Props
123+
124+
```ts
125+
interface OriginalProps {
126+
/**
127+
The initial document. Defaults to an empty document. Can be
128+
provided either as a plain string (which will be split into
129+
lines according to the value of the [`lineSeparator`
130+
facet](https://codemirror.net/6/docs/ref/#state.EditorState^lineSeparator)), or an instance of
131+
the [`Text`](https://codemirror.net/6/docs/ref/#state.Text) class (which is what the state will use
132+
to represent the document).
133+
*/
134+
value?: string | Text;
135+
/**
136+
The starting selection. Defaults to a cursor at the very start
137+
of the document.
138+
*/
139+
selection?:
140+
| EditorSelection
141+
| {
142+
anchor: number;
143+
head?: number;
144+
};
145+
/**
146+
[Extension(s)](https://codemirror.net/6/docs/ref/#state.Extension) to associate with this state.
147+
*/
148+
extensions?: Extension;
149+
}
150+
```
151+
49152
## Contributors
50153

51154
As always, thanks to our amazing contributors!

merge/src/Internal.tsx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
1-
import React, { useEffect, useImperativeHandle, useRef } from 'react';
2-
import { MergeView } from '@codemirror/merge';
1+
import React, { useEffect, useImperativeHandle, useMemo, useRef, memo } from 'react';
2+
import { MergeView, MergeConfig } from '@codemirror/merge';
33
import { useStore } from './store';
4+
import { CodeMirrorMergeProps } from './';
45

56
export interface InternalRef {
67
container?: HTMLDivElement | null;
78
view?: MergeView;
89
}
910

10-
export interface InternalProps extends React.LiHTMLAttributes<HTMLDivElement> {}
11-
12-
export const Internal = React.forwardRef((props: InternalProps, ref?: React.ForwardedRef<InternalRef>) => {
13-
const { className, children } = props;
14-
const { modified, original, view, dispatch } = useStore();
11+
export const Internal = React.forwardRef((props: CodeMirrorMergeProps, ref?: React.ForwardedRef<InternalRef>) => {
12+
const {
13+
className,
14+
children,
15+
orientation,
16+
revertControls,
17+
highlightChanges,
18+
gutter,
19+
collapseUnchanged,
20+
renderRevertControl,
21+
...elmProps
22+
} = props;
23+
const { modified, original, view, dispatch, ...otherStore } = useStore();
1524
const editor = useRef<HTMLDivElement>(null);
1625
useImperativeHandle(ref, () => ({ container: editor.current, view }), [editor, view]);
1726
useEffect(() => {
1827
if (!view && editor.current && original && modified) {
28+
const opts = { orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl };
1929
const viewDefault = new MergeView({
2030
a: original,
2131
b: modified,
2232
parent: editor.current,
33+
...opts,
2334
});
24-
dispatch && dispatch({ view: viewDefault });
35+
dispatch && dispatch({ view: viewDefault, ...opts });
2536
}
2637
}, [editor.current, original, modified, view]);
2738

@@ -31,9 +42,51 @@ export const Internal = React.forwardRef((props: InternalProps, ref?: React.Forw
3142
};
3243
}, []);
3344

45+
useEffect(() => {
46+
if (view) {
47+
const opts: MergeConfig = {};
48+
if (otherStore.orientation !== orientation) {
49+
opts.orientation = orientation;
50+
}
51+
if (otherStore.revertControls !== revertControls) {
52+
opts.revertControls = revertControls;
53+
}
54+
if (otherStore.highlightChanges !== highlightChanges) {
55+
opts.highlightChanges = highlightChanges;
56+
}
57+
if (otherStore.gutter !== gutter) {
58+
opts.gutter = gutter;
59+
}
60+
if (otherStore.collapseUnchanged !== collapseUnchanged) {
61+
opts.collapseUnchanged = collapseUnchanged;
62+
}
63+
if (Object.keys(opts).length && dispatch && original && modified && editor.current) {
64+
view.destroy();
65+
const viewDefault = new MergeView({
66+
a: original,
67+
b: modified,
68+
parent: editor.current,
69+
...opts,
70+
});
71+
dispatch({ ...opts, renderRevertControl, view: viewDefault });
72+
}
73+
}
74+
}, [
75+
view,
76+
original,
77+
modified,
78+
editor,
79+
orientation,
80+
revertControls,
81+
highlightChanges,
82+
gutter,
83+
collapseUnchanged,
84+
renderRevertControl,
85+
]);
86+
3487
const defaultClassNames = 'cm-merge-theme';
3588
return (
36-
<div ref={editor} className={`${defaultClassNames}${className ? ` ${className}` : ''}`} {...props}>
89+
<div ref={editor} className={`${defaultClassNames}${className ? ` ${className}` : ''}`} {...elmProps}>
3790
{children}
3891
</div>
3992
);

merge/src/Modified.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EditorStateConfig, Extension, StateEffect } from '@codemirror/state';
33
import { getDefaultExtensions } from '@uiw/react-codemirror';
44
import { useStore } from './store';
55

6-
export interface ModifiedProps extends EditorStateConfig {
6+
export interface ModifiedProps extends Omit<EditorStateConfig, 'doc'> {
77
value?: EditorStateConfig['doc'];
88
extensions?: Extension[];
99
}
@@ -21,20 +21,15 @@ export const Modified = (props: ModifiedProps): JSX.Element | null => {
2121
if (modifiedDoc !== props.value) {
2222
view.b.dispatch({
2323
changes: { from: 0, to: (modifiedDoc || '').length, insert: props.value || '' },
24+
effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]),
2425
});
2526
}
2627
}
2728
if (modified?.selection !== props.selection) {
2829
data.selection = props.selection;
2930
dispatch!({ modified: { ...modified, ...data } });
3031
}
31-
}, [props.value, props.selection, view]);
32-
33-
useEffect(() => {
34-
if (view) {
35-
view.b.dispatch({ effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]) });
36-
}
37-
}, [extensions, view]);
32+
}, [props.value, extensions, props.selection, view]);
3833

3934
return null;
4035
};

merge/src/Original.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EditorStateConfig, Extension, StateEffect } from '@codemirror/state';
33
import { useStore } from './store';
44
import { getDefaultExtensions } from '@uiw/react-codemirror';
55

6-
export interface OriginalProps extends EditorStateConfig {
6+
export interface OriginalProps extends Omit<EditorStateConfig, 'doc'> {
77
value?: EditorStateConfig['doc'];
88
extensions?: Extension[];
99
}
@@ -21,6 +21,7 @@ export const Original = (props: OriginalProps): JSX.Element | null => {
2121
if (originalDoc !== props.value) {
2222
view?.a.dispatch({
2323
changes: { from: 0, to: (originalDoc || '').length, insert: props.value || '' },
24+
effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]),
2425
});
2526
}
2627
}
@@ -30,12 +31,6 @@ export const Original = (props: OriginalProps): JSX.Element | null => {
3031
}
3132
}, [props.value, props.selection, view]);
3233

33-
useEffect(() => {
34-
if (view) {
35-
view.a.dispatch({ effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]) });
36-
}
37-
}, [extensions, view]);
38-
3934
return null;
4035
};
4136

merge/src/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import React from 'react';
2+
import { MergeConfig } from '@codemirror/merge';
23
import { Original } from './Original';
34
import { Modified } from './Modified';
45
import { Internal, InternalRef } from './Internal';
56
import { Provider } from './store';
67

7-
export interface ReactCodeMirrorMergeRef extends InternalRef {}
8-
export interface ReactCodeMirrorMergeProps extends React.LiHTMLAttributes<HTMLDivElement> {}
8+
export interface CodeMirrorMergeRef extends InternalRef {}
9+
export interface CodeMirrorMergeProps extends React.HTMLAttributes<HTMLDivElement>, MergeConfig {}
910

10-
const InternalCodeMirror = (props: ReactCodeMirrorMergeProps, ref?: React.ForwardedRef<InternalRef>) => {
11+
const InternalCodeMirror = (props: CodeMirrorMergeProps, ref?: React.ForwardedRef<InternalRef>) => {
1112
return (
1213
<Provider>
1314
<Internal {...props} ref={ref} />
1415
</Provider>
1516
);
1617
};
1718

18-
type CodeMirrorComponent = React.FC<React.PropsWithRef<ReactCodeMirrorMergeProps>> & {
19+
type CodeMirrorComponent = React.FC<React.PropsWithRef<CodeMirrorMergeProps>> & {
1920
Original: typeof Original;
2021
Modified: typeof Modified;
2122
};

merge/src/store.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { PropsWithChildren, createContext, useContext, useReducer } from 'react';
22
import { EditorStateConfig } from '@codemirror/state';
3-
import { MergeView } from '@codemirror/merge';
3+
import { MergeView, MergeConfig } from '@codemirror/merge';
44

55
export interface StoreContextValue extends InitialState {
66
dispatch?: React.Dispatch<InitialState>;
77
}
88

9-
export interface InitialState {
9+
export interface InitialState extends MergeConfig {
1010
modified?: EditorStateConfig;
1111
original?: EditorStateConfig;
1212
view?: MergeView;

0 commit comments

Comments
 (0)