diff --git a/merge/README.md b/merge/README.md index 07e4e5f46..6d502e135 100644 --- a/merge/README.md +++ b/merge/README.md @@ -35,7 +35,7 @@ five`; export const Example = () => { return ( - + { }; ``` +## Props + +```ts +export interface CodeMirrorMergeProps extends React.HTMLAttributes, MergeConfig {} + +interface MergeConfig { + /** + Controls whether editor A or editor B is shown first. Defaults + to `"a-b"`. + */ + orientation?: 'a-b' | 'b-a'; + /** + Controls whether revert controls are shown between changed + chunks. + */ + revertControls?: 'a-to-b' | 'b-to-a'; + /** + When given, this function is called to render the button to + revert a chunk. + */ + renderRevertControl?: () => HTMLElement; + /** + By default, the merge view will mark inserted and deleted text + in changed chunks. Set this to false to turn that off. + */ + highlightChanges?: boolean; + /** + Controls whether a gutter marker is shown next to changed lines. + */ + gutter?: boolean; + /** + When given, long stretches of unchanged text are collapsed. + `margin` gives the number of lines to leave visible after/before + a change (default is 3), and `minSize` gives the minimum amount + of collapsible lines that need to be present (defaults to 4). + */ + collapseUnchanged?: { + margin?: number; + minSize?: number; + }; +} +``` + +## Modified Props + +```ts +interface ModifiedProps { + /** + The initial document. Defaults to an empty document. Can be + provided either as a plain string (which will be split into + lines according to the value of the [`lineSeparator` + facet](https://codemirror.net/6/docs/ref/#state.EditorState^lineSeparator)), or an instance of + the [`Text`](https://codemirror.net/6/docs/ref/#state.Text) class (which is what the state will use + to represent the document). + */ + value?: string | Text; + /** + The starting selection. Defaults to a cursor at the very start + of the document. + */ + selection?: + | EditorSelection + | { + anchor: number; + head?: number; + }; + /** + [Extension(s)](https://codemirror.net/6/docs/ref/#state.Extension) to associate with this state. + */ + extensions?: Extension; +} +``` + +## Original Props + +```ts +interface OriginalProps { + /** + The initial document. Defaults to an empty document. Can be + provided either as a plain string (which will be split into + lines according to the value of the [`lineSeparator` + facet](https://codemirror.net/6/docs/ref/#state.EditorState^lineSeparator)), or an instance of + the [`Text`](https://codemirror.net/6/docs/ref/#state.Text) class (which is what the state will use + to represent the document). + */ + value?: string | Text; + /** + The starting selection. Defaults to a cursor at the very start + of the document. + */ + selection?: + | EditorSelection + | { + anchor: number; + head?: number; + }; + /** + [Extension(s)](https://codemirror.net/6/docs/ref/#state.Extension) to associate with this state. + */ + extensions?: Extension; +} +``` + ## Contributors As always, thanks to our amazing contributors! diff --git a/merge/src/Internal.tsx b/merge/src/Internal.tsx index 1a8599110..7bce755c3 100644 --- a/merge/src/Internal.tsx +++ b/merge/src/Internal.tsx @@ -1,27 +1,38 @@ -import React, { useEffect, useImperativeHandle, useRef } from 'react'; -import { MergeView } from '@codemirror/merge'; +import React, { useEffect, useImperativeHandle, useMemo, useRef, memo } from 'react'; +import { MergeView, MergeConfig } from '@codemirror/merge'; import { useStore } from './store'; +import { CodeMirrorMergeProps } from './'; export interface InternalRef { container?: HTMLDivElement | null; view?: MergeView; } -export interface InternalProps extends React.LiHTMLAttributes {} - -export const Internal = React.forwardRef((props: InternalProps, ref?: React.ForwardedRef) => { - const { className, children } = props; - const { modified, original, view, dispatch } = useStore(); +export const Internal = React.forwardRef((props: CodeMirrorMergeProps, ref?: React.ForwardedRef) => { + const { + className, + children, + orientation, + revertControls, + highlightChanges, + gutter, + collapseUnchanged, + renderRevertControl, + ...elmProps + } = props; + const { modified, original, view, dispatch, ...otherStore } = useStore(); const editor = useRef(null); useImperativeHandle(ref, () => ({ container: editor.current, view }), [editor, view]); useEffect(() => { if (!view && editor.current && original && modified) { + const opts = { orientation, revertControls, highlightChanges, gutter, collapseUnchanged, renderRevertControl }; const viewDefault = new MergeView({ a: original, b: modified, parent: editor.current, + ...opts, }); - dispatch && dispatch({ view: viewDefault }); + dispatch && dispatch({ view: viewDefault, ...opts }); } }, [editor.current, original, modified, view]); @@ -31,9 +42,51 @@ export const Internal = React.forwardRef((props: InternalProps, ref?: React.Forw }; }, []); + useEffect(() => { + if (view) { + const opts: MergeConfig = {}; + if (otherStore.orientation !== orientation) { + opts.orientation = orientation; + } + if (otherStore.revertControls !== revertControls) { + opts.revertControls = revertControls; + } + if (otherStore.highlightChanges !== highlightChanges) { + opts.highlightChanges = highlightChanges; + } + if (otherStore.gutter !== gutter) { + opts.gutter = gutter; + } + 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 }); + } + } + }, [ + view, + original, + modified, + editor, + orientation, + revertControls, + highlightChanges, + gutter, + collapseUnchanged, + renderRevertControl, + ]); + const defaultClassNames = 'cm-merge-theme'; return ( -
+
{children}
); diff --git a/merge/src/Modified.tsx b/merge/src/Modified.tsx index 62503bb3e..d0d46ff3b 100644 --- a/merge/src/Modified.tsx +++ b/merge/src/Modified.tsx @@ -3,7 +3,7 @@ import { EditorStateConfig, Extension, StateEffect } from '@codemirror/state'; import { getDefaultExtensions } from '@uiw/react-codemirror'; import { useStore } from './store'; -export interface ModifiedProps extends EditorStateConfig { +export interface ModifiedProps extends Omit { value?: EditorStateConfig['doc']; extensions?: Extension[]; } @@ -21,6 +21,7 @@ export const Modified = (props: ModifiedProps): JSX.Element | null => { if (modifiedDoc !== props.value) { view.b.dispatch({ changes: { from: 0, to: (modifiedDoc || '').length, insert: props.value || '' }, + effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]), }); } } @@ -28,13 +29,7 @@ export const Modified = (props: ModifiedProps): JSX.Element | null => { data.selection = props.selection; dispatch!({ modified: { ...modified, ...data } }); } - }, [props.value, props.selection, view]); - - useEffect(() => { - if (view) { - view.b.dispatch({ effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]) }); - } - }, [extensions, view]); + }, [props.value, extensions, props.selection, view]); return null; }; diff --git a/merge/src/Original.tsx b/merge/src/Original.tsx index 1290bb03a..9e49b1852 100644 --- a/merge/src/Original.tsx +++ b/merge/src/Original.tsx @@ -3,7 +3,7 @@ import { EditorStateConfig, Extension, StateEffect } from '@codemirror/state'; import { useStore } from './store'; import { getDefaultExtensions } from '@uiw/react-codemirror'; -export interface OriginalProps extends EditorStateConfig { +export interface OriginalProps extends Omit { value?: EditorStateConfig['doc']; extensions?: Extension[]; } @@ -21,6 +21,7 @@ export const Original = (props: OriginalProps): JSX.Element | null => { if (originalDoc !== props.value) { view?.a.dispatch({ changes: { from: 0, to: (originalDoc || '').length, insert: props.value || '' }, + effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]), }); } } @@ -30,12 +31,6 @@ export const Original = (props: OriginalProps): JSX.Element | null => { } }, [props.value, props.selection, view]); - useEffect(() => { - if (view) { - view.a.dispatch({ effects: StateEffect.appendConfig.of([...defaultExtensions, ...extensions]) }); - } - }, [extensions, view]); - return null; }; diff --git a/merge/src/index.tsx b/merge/src/index.tsx index f8e6a3391..db1c2e3c2 100644 --- a/merge/src/index.tsx +++ b/merge/src/index.tsx @@ -1,13 +1,14 @@ import React from 'react'; +import { MergeConfig } from '@codemirror/merge'; import { Original } from './Original'; import { Modified } from './Modified'; import { Internal, InternalRef } from './Internal'; import { Provider } from './store'; -export interface ReactCodeMirrorMergeRef extends InternalRef {} -export interface ReactCodeMirrorMergeProps extends React.LiHTMLAttributes {} +export interface CodeMirrorMergeRef extends InternalRef {} +export interface CodeMirrorMergeProps extends React.HTMLAttributes, MergeConfig {} -const InternalCodeMirror = (props: ReactCodeMirrorMergeProps, ref?: React.ForwardedRef) => { +const InternalCodeMirror = (props: CodeMirrorMergeProps, ref?: React.ForwardedRef) => { return ( @@ -15,7 +16,7 @@ const InternalCodeMirror = (props: ReactCodeMirrorMergeProps, ref?: React.Forwar ); }; -type CodeMirrorComponent = React.FC> & { +type CodeMirrorComponent = React.FC> & { Original: typeof Original; Modified: typeof Modified; }; diff --git a/merge/src/store.tsx b/merge/src/store.tsx index 913838279..d3a4d5d68 100644 --- a/merge/src/store.tsx +++ b/merge/src/store.tsx @@ -1,12 +1,12 @@ import React, { PropsWithChildren, createContext, useContext, useReducer } from 'react'; import { EditorStateConfig } from '@codemirror/state'; -import { MergeView } from '@codemirror/merge'; +import { MergeView, MergeConfig } from '@codemirror/merge'; export interface StoreContextValue extends InitialState { dispatch?: React.Dispatch; } -export interface InitialState { +export interface InitialState extends MergeConfig { modified?: EditorStateConfig; original?: EditorStateConfig; view?: MergeView; diff --git a/www/src/pages/merge/Example.tsx b/www/src/pages/merge/Example.tsx index ccd294e80..b60f469c6 100644 --- a/www/src/pages/merge/Example.tsx +++ b/www/src/pages/merge/Example.tsx @@ -1,23 +1,81 @@ -import CodeMirrorMerge from 'react-codemirror-merge'; +import { Fragment, useState } from 'react'; +import CodeMirrorMerge, { CodeMirrorMergeProps } from 'react-codemirror-merge'; import { EditorView } from 'codemirror'; import { EditorState } from '@codemirror/state'; +import { langs } from '@uiw/codemirror-extensions-langs'; + +import { originalCode, modifiedCode } from './code'; const Original = CodeMirrorMerge.Original; const Modified = CodeMirrorMerge.Modified; -let doc = `one -two -three -four -five`; export const Example = () => { + const [orientation, setOrientation] = useState('a-b'); + const [revertControls, setRevertControls] = useState(); + const [highlightChanges, setHighlightChanges] = useState(true); + const [gutter, setGutter] = useState(true); + const [collapseUnchanged, setCollapseUnchanged] = useState({}); + const handleOrientation = (evn: React.ChangeEvent) => { + setOrientation(evn.target.value as CodeMirrorMergeProps['orientation']); + }; return ( - - - - + + + + + + +
+ +
+ +
+ + +
); }; diff --git a/www/src/pages/merge/code.ts b/www/src/pages/merge/code.ts new file mode 100644 index 000000000..f6d3b2a71 --- /dev/null +++ b/www/src/pages/merge/code.ts @@ -0,0 +1,74 @@ +export const originalCode = `// The player has a position, a size, and a current speed. +class Player { + size = new Vec(0.8, 1.5); + + constructor(pos, speed) { + this.pos = pos; + this.speed = speed; + } + + static create(pos) { + return new Player(pos.plus(new Vec(0, -0.5)), new Vec(0, 0)); + } +} + +// Lava block. When you touch it, you die. +class Lava { + size = new Vec(1, 1) + + constructor(pos, speed, reset) { + this.pos = pos; + this.speed = speed; + this.reset = reset; + } + + static horizontal(pos) { + return new Lava(pos, new Vec(2, 0)); + } + + static vertical(pos) { + return new Lava(pos, new Vec(0, 2)); + } + + static drip(pos) { + return new Lava(pos, new Vec(0, 3), pos); + } +} +`; + +export const modifiedCode = `class Player { + get type() { return "player" } + + constructor(pos, speed) { + this.pos = pos; + this.speed = speed; + } + + static create(pos) { + return new Player(pos.plus(new Vec(0, -0.5)), new Vec(0, 0)); + } +} + +class Lava { + constructor(pos, speed, reset) { + this.pos = pos; + this.speed = speed; + this.reset = reset; + } + + get type() { return "lava"; } + + static create(pos, ch) { + if (ch == "=") { + return new Lava(pos, new Vec(2, 0)); + } else if (ch == "|") { + return new Lava(pos, new Vec(0, 2)); + } else if (ch == "v") { + return new Lava(pos, new Vec(0, 3), pos); + } + } +} + +Player.prototype.size = new Vec(0.8, 1.5); +Lava.prototype.size = new Vec(1, 1); +`; diff --git a/www/src/pages/merge/index.tsx b/www/src/pages/merge/index.tsx index 77340e477..f4d1062bb 100644 --- a/www/src/pages/merge/index.tsx +++ b/www/src/pages/merge/index.tsx @@ -15,10 +15,7 @@ export const MergeDoc = () => { return ( - + Top );