Skip to content

Commit 3198a3c

Browse files
committedApr 8, 2023
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

+104-1
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

+62-9
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

+3-8
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

+2-7
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

+5-4
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

+2-2
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;

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

+71-13
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,81 @@
1-
import CodeMirrorMerge from 'react-codemirror-merge';
1+
import { Fragment, useState } from 'react';
2+
import CodeMirrorMerge, { CodeMirrorMergeProps } from 'react-codemirror-merge';
23
import { EditorView } from 'codemirror';
34
import { EditorState } from '@codemirror/state';
5+
import { langs } from '@uiw/codemirror-extensions-langs';
6+
7+
import { originalCode, modifiedCode } from './code';
48

59
const Original = CodeMirrorMerge.Original;
610
const Modified = CodeMirrorMerge.Modified;
7-
let doc = `one
8-
two
9-
three
10-
four
11-
five`;
1211

1312
export const Example = () => {
13+
const [orientation, setOrientation] = useState<CodeMirrorMergeProps['orientation']>('a-b');
14+
const [revertControls, setRevertControls] = useState<CodeMirrorMergeProps['revertControls']>();
15+
const [highlightChanges, setHighlightChanges] = useState<CodeMirrorMergeProps['highlightChanges']>(true);
16+
const [gutter, setGutter] = useState<CodeMirrorMergeProps['gutter']>(true);
17+
const [collapseUnchanged, setCollapseUnchanged] = useState<CodeMirrorMergeProps['collapseUnchanged']>({});
18+
const handleOrientation = (evn: React.ChangeEvent<HTMLSelectElement>) => {
19+
setOrientation(evn.target.value as CodeMirrorMergeProps['orientation']);
20+
};
1421
return (
15-
<CodeMirrorMerge>
16-
<Original value={doc} />
17-
<Modified
18-
value={doc.replace(/t/g, 'T') + 'Six'}
19-
extensions={[EditorView.editable.of(false), EditorState.readOnly.of(true)]}
20-
/>
21-
</CodeMirrorMerge>
22+
<Fragment>
23+
<CodeMirrorMerge
24+
orientation={orientation}
25+
revertControls={revertControls}
26+
collapseUnchanged={collapseUnchanged}
27+
highlightChanges={highlightChanges}
28+
gutter={gutter}
29+
style={{ height: 300, overflow: 'auto' }}
30+
>
31+
<Original value={originalCode} extensions={[langs.javascript()]} />
32+
<Modified
33+
value={modifiedCode}
34+
extensions={[langs.javascript(), EditorView.editable.of(false), EditorState.readOnly.of(true)]}
35+
/>
36+
</CodeMirrorMerge>
37+
<label>
38+
Orientation
39+
<select onChange={handleOrientation} defaultValue={orientation}>
40+
<option value="">please orientation choose</option>
41+
<option value="a-b">a-b</option>
42+
<option value="b-a">b-a</option>
43+
</select>
44+
</label>
45+
<br />
46+
<label>
47+
Revert buttons
48+
<select
49+
defaultValue={revertControls}
50+
onChange={(evn) => setRevertControls(evn.target.value as CodeMirrorMergeProps['revertControls'])}
51+
>
52+
<option value="">please revertControls choose</option>
53+
<option value="a-to-b">a-to-b</option>
54+
<option value="b-to-a">b-to-a</option>
55+
</select>
56+
</label>
57+
<br />
58+
<label>
59+
Highlight changes
60+
<input
61+
type="checkbox"
62+
checked={!!highlightChanges}
63+
onChange={(evn) => setHighlightChanges(evn.target.checked)}
64+
/>
65+
</label>
66+
<br />
67+
<label>
68+
Gutter markers
69+
<input type="checkbox" checked={!!gutter} onChange={(evn) => setGutter(evn.target.checked)} />
70+
</label>
71+
<label>
72+
Collapse unchanged code
73+
<input
74+
type="checkbox"
75+
checked={!!collapseUnchanged}
76+
onChange={(evn) => setCollapseUnchanged(evn.target.checked ? {} : undefined)}
77+
/>
78+
</label>
79+
</Fragment>
2280
);
2381
};

‎www/src/pages/merge/code.ts

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
export const originalCode = `// The player has a position, a size, and a current speed.
2+
class Player {
3+
size = new Vec(0.8, 1.5);
4+
5+
constructor(pos, speed) {
6+
this.pos = pos;
7+
this.speed = speed;
8+
}
9+
10+
static create(pos) {
11+
return new Player(pos.plus(new Vec(0, -0.5)), new Vec(0, 0));
12+
}
13+
}
14+
15+
// Lava block. When you touch it, you die.
16+
class Lava {
17+
size = new Vec(1, 1)
18+
19+
constructor(pos, speed, reset) {
20+
this.pos = pos;
21+
this.speed = speed;
22+
this.reset = reset;
23+
}
24+
25+
static horizontal(pos) {
26+
return new Lava(pos, new Vec(2, 0));
27+
}
28+
29+
static vertical(pos) {
30+
return new Lava(pos, new Vec(0, 2));
31+
}
32+
33+
static drip(pos) {
34+
return new Lava(pos, new Vec(0, 3), pos);
35+
}
36+
}
37+
`;
38+
39+
export const modifiedCode = `class Player {
40+
get type() { return "player" }
41+
42+
constructor(pos, speed) {
43+
this.pos = pos;
44+
this.speed = speed;
45+
}
46+
47+
static create(pos) {
48+
return new Player(pos.plus(new Vec(0, -0.5)), new Vec(0, 0));
49+
}
50+
}
51+
52+
class Lava {
53+
constructor(pos, speed, reset) {
54+
this.pos = pos;
55+
this.speed = speed;
56+
this.reset = reset;
57+
}
58+
59+
get type() { return "lava"; }
60+
61+
static create(pos, ch) {
62+
if (ch == "=") {
63+
return new Lava(pos, new Vec(2, 0));
64+
} else if (ch == "|") {
65+
return new Lava(pos, new Vec(0, 2));
66+
} else if (ch == "v") {
67+
return new Lava(pos, new Vec(0, 3), pos);
68+
}
69+
}
70+
}
71+
72+
Player.prototype.size = new Vec(0.8, 1.5);
73+
Lava.prototype.size = new Vec(1, 1);
74+
`;

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ export const MergeDoc = () => {
1515
return (
1616
<PageWarpper>
1717
<Example />
18-
<MarkdownPreview
19-
source={mdSource.document}
20-
style={{ width: 980, margin: '0 auto', padding: '50px 20px 150px 20px' }}
21-
/>
18+
<MarkdownPreview source={mdSource.document} style={{ width: 980, margin: '0 auto', padding: '50px 0 150px 0' }} />
2219
<BackToUp>Top</BackToUp>
2320
</PageWarpper>
2421
);

1 commit comments

Comments
 (1)

jaywcjlove commented on Apr 8, 2023

@jaywcjlove
MemberAuthor
Please sign in to comment.