diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 11b220382..5d338366f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -92,6 +92,12 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - name: 📦 react-codemirror-merge publish to NPM
+ run: npm publish --access public
+ working-directory: ./merge/
+ continue-on-error: true
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: 📦 @uiw/codemirror-themes publish to NPM
run: npm publish --access public
@@ -364,6 +370,20 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+ - name: "Modify react-codemirror-merge => @uiwjs/react-codemirror-merge"
+ uses: jaywcjlove/github-action-package@main
+ continue-on-error: true
+ with:
+ path: merge/package.json
+ rename: "@uiwjs/react-codemirror-merge"
+
+ - run: npm publish
+ name: 📦 @uiwjs/react-codemirror-merge publish to NPM
+ working-directory: merge
+ continue-on-error: true
+ env:
+ NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+
- name: Modify @uiw/codemirror-themes => @uiwjs/codemirror-themes
uses: jaywcjlove/github-action-package@main
with:
diff --git a/core/README.md b/core/README.md
index e5eb640ba..5c49568d6 100644
--- a/core/README.md
+++ b/core/README.md
@@ -183,6 +183,34 @@ export default function App() {
}
```
+## Codemirror Merge
+
+```jsx
+import CodeMirrorMerge from 'react-codemirror-merge';
+import { EditorView } from 'codemirror';
+import { EditorState } from '@codemirror/state';
+
+const Original = CodeMirrorMerge.Original;
+const Modified = CodeMirrorMerge.Modified;
+let doc = `one
+two
+three
+four
+five`;
+
+export const Example = () => {
+ return (
+
+
+
+
+ );
+};
+```
+
## Support Hook
[![Open in CodeSandbox](https://img.shields.io/badge/Open%20in-CodeSandbox-blue?logo=codesandbox)](https://codesandbox.io/embed/react-codemirror-example-codemirror-6-hook-yr4vg?fontsize=14&hidenavigation=1&theme=dark)
@@ -397,7 +425,7 @@ export interface ReactCodeMirrorProps
*/
readOnly?: boolean;
/**
- * Controls whether pressing the `Tab` key inserts a tab character and indents the text (`true`)
+ * Controls whether pressing the `Tab` key inserts a tab character and indents the text (`true`)
* or behaves according to the browser's default behavior (`false`).
* @default true
*/
diff --git a/core/src/getDefaultExtensions.ts b/core/src/getDefaultExtensions.ts
new file mode 100644
index 000000000..9248785ac
--- /dev/null
+++ b/core/src/getDefaultExtensions.ts
@@ -0,0 +1,71 @@
+import { Extension } from '@codemirror/state';
+import { indentWithTab } from '@codemirror/commands';
+import { basicSetup, BasicSetupOptions } from '@uiw/codemirror-extensions-basic-setup';
+import { EditorView, keymap, placeholder } from '@codemirror/view';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { EditorState } from '@codemirror/state';
+
+export type DefaultExtensionsOptions = {
+ indentWithTab?: boolean;
+ basicSetup?: boolean | BasicSetupOptions;
+ placeholder?: string | HTMLElement;
+ theme?: 'light' | 'dark' | 'none' | Extension;
+ readOnly?: boolean;
+ editable?: boolean;
+};
+
+export const getDefaultExtensions = (optios: DefaultExtensionsOptions = {}): Extension[] => {
+ const {
+ indentWithTab: defaultIndentWithTab = true,
+ editable = true,
+ readOnly = false,
+ theme = 'light',
+ placeholder: placeholderStr = '',
+ basicSetup: defaultBasicSetup = true,
+ } = optios;
+ const getExtensions: Extension[] = [];
+ const defaultLightThemeOption = EditorView.theme(
+ {
+ '&': {
+ backgroundColor: '#fff',
+ },
+ },
+ {
+ dark: false,
+ },
+ );
+ if (defaultIndentWithTab) {
+ getExtensions.unshift(keymap.of([indentWithTab]));
+ }
+ if (defaultBasicSetup) {
+ if (typeof defaultBasicSetup === 'boolean') {
+ getExtensions.unshift(basicSetup());
+ } else {
+ getExtensions.unshift(basicSetup(defaultBasicSetup));
+ }
+ }
+ if (placeholderStr) {
+ getExtensions.unshift(placeholder(placeholderStr));
+ }
+ switch (theme) {
+ case 'light':
+ getExtensions.push(defaultLightThemeOption);
+ break;
+ case 'dark':
+ getExtensions.push(oneDark);
+ break;
+ case 'none':
+ break;
+ default:
+ getExtensions.push(theme);
+ break;
+ }
+ if (editable === false) {
+ getExtensions.push(EditorView.editable.of(false));
+ }
+ if (readOnly) {
+ getExtensions.push(EditorState.readOnly.of(true));
+ }
+
+ return [...getExtensions];
+};
diff --git a/core/src/index.tsx b/core/src/index.tsx
index 62cfd97fb..898e31576 100644
--- a/core/src/index.tsx
+++ b/core/src/index.tsx
@@ -7,6 +7,7 @@ import { Statistics } from './utils';
export * from '@uiw/codemirror-extensions-basic-setup';
export * from './useCodeMirror';
+export * from './getDefaultExtensions';
export * from './utils';
export interface ReactCodeMirrorProps
@@ -45,7 +46,7 @@ export interface ReactCodeMirrorProps
*/
readOnly?: boolean;
/**
- * Controls whether pressing the `Tab` key inserts a tab character and indents the text (`true`)
+ * Controls whether pressing the `Tab` key inserts a tab character and indents the text (`true`)
* or behaves according to the browser's default behavior (`false`).
* @default true
*/
diff --git a/core/src/useCodeMirror.ts b/core/src/useCodeMirror.ts
index 4b9a84723..a34aaa458 100644
--- a/core/src/useCodeMirror.ts
+++ b/core/src/useCodeMirror.ts
@@ -1,9 +1,7 @@
import { useEffect, useState } from 'react';
import { Annotation, EditorState, StateEffect } from '@codemirror/state';
-import { indentWithTab } from '@codemirror/commands';
-import { EditorView, keymap, ViewUpdate, placeholder } from '@codemirror/view';
-import { basicSetup } from '@uiw/codemirror-extensions-basic-setup';
-import { oneDark } from '@codemirror/theme-one-dark';
+import { EditorView, ViewUpdate } from '@codemirror/view';
+import { getDefaultExtensions } from './getDefaultExtensions';
import { getStatistics } from './utils';
import { ReactCodeMirrorProps } from '.';
@@ -41,16 +39,6 @@ export function useCodeMirror(props: UseCodeMirror) {
const [container, setContainer] = useState();
const [view, setView] = useState();
const [state, setState] = useState();
- const defaultLightThemeOption = EditorView.theme(
- {
- '&': {
- backgroundColor: '#fff',
- },
- },
- {
- dark: false,
- },
- );
const defaultThemeOption = EditorView.theme({
'&': {
height,
@@ -76,42 +64,16 @@ export function useCodeMirror(props: UseCodeMirror) {
onStatistics && onStatistics(getStatistics(vu));
});
- let getExtensions = [updateListener, defaultThemeOption];
- if (defaultIndentWithTab) {
- getExtensions.unshift(keymap.of([indentWithTab]));
- }
- if (defaultBasicSetup) {
- if (typeof defaultBasicSetup === 'boolean') {
- getExtensions.unshift(basicSetup());
- } else {
- getExtensions.unshift(basicSetup(defaultBasicSetup));
- }
- }
-
- if (placeholderStr) {
- getExtensions.unshift(placeholder(placeholderStr));
- }
-
- switch (theme) {
- case 'light':
- getExtensions.push(defaultLightThemeOption);
- break;
- case 'dark':
- getExtensions.push(oneDark);
- break;
- case 'none':
- break;
- default:
- getExtensions.push(theme);
- break;
- }
+ const defaultExtensions = getDefaultExtensions({
+ theme,
+ editable: true,
+ readOnly: false,
+ placeholder: placeholderStr,
+ indentWithTab: defaultIndentWithTab,
+ basicSetup: defaultBasicSetup,
+ });
- if (editable === false) {
- getExtensions.push(EditorView.editable.of(false));
- }
- if (readOnly) {
- getExtensions.push(EditorState.readOnly.of(true));
- }
+ let getExtensions = [updateListener, defaultThemeOption, ...defaultExtensions];
if (onUpdate && typeof onUpdate === 'function') {
getExtensions.push(EditorView.updateListener.of(onUpdate));
diff --git a/lerna.json b/lerna.json
index 70c2e60f1..4df64b383 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
"version": "4.19.11",
- "packages": ["themes/**", "core", "www"],
+ "packages": ["themes/**", "core", "merge", "www"],
"useWorkspaces": true
}
diff --git a/merge/README.md b/merge/README.md
new file mode 100644
index 000000000..c6a9b9ec9
--- /dev/null
+++ b/merge/README.md
@@ -0,0 +1,57 @@
+
+
+# react-codemirror-merge
+
+
+
+[![npm version](https://img.shields.io/npm/v/react-codemirror-merge.svg)](https://www.npmjs.com/package/react-codemirror-merge)
+
+CodeMirror merge view for React.
+
+## Install
+
+```bash
+npm install react-codemirror-merge --save
+```
+
+## Usage
+
+```jsx
+import CodeMirrorMerge from 'react-codemirror-merge';
+import { EditorView } from 'codemirror';
+import { EditorState } from '@codemirror/state';
+
+const Original = CodeMirrorMerge.Original;
+const Modified = CodeMirrorMerge.Modified;
+let doc = `one
+two
+three
+four
+five`;
+
+export const Example = () => {
+ return (
+
+
+
+
+ );
+};
+```
+
+## Contributors
+
+As always, thanks to our amazing contributors!
+
+
+
+
+
+Made with [github-action-contributors](https://github.com/jaywcjlove/github-action-contributors).
+
+## License
+
+Licensed under the MIT License.
diff --git a/merge/package.json b/merge/package.json
new file mode 100644
index 000000000..2348fd795
--- /dev/null
+++ b/merge/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "react-codemirror-merge",
+ "version": "0.0.1",
+ "description": "CodeMirror merge view for React.",
+ "homepage": "https://uiwjs.github.io/react-codemirror",
+ "author": "kenny wong ",
+ "license": "MIT",
+ "main": "./cjs/index.js",
+ "module": "./esm/index.js",
+ "scripts": {
+ "watch": "tsbb watch src/*.tsx --use-babel",
+ "build": "tsbb build src/*.tsx --use-babel",
+ "test": "tsbb test --env=jsdom",
+ "coverage": "tsbb test --env=jsdom --coverage --bail"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/uiwjs/react-codemirror.git"
+ },
+ "files": [
+ "dist",
+ "src",
+ "esm",
+ "cjs"
+ ],
+ "peerDependencies": {
+ "@babel/runtime": ">=7.11.0",
+ "@codemirror/state": ">=6.0.0",
+ "@codemirror/theme-one-dark": ">=6.0.0",
+ "@codemirror/view": ">=6.0.0",
+ "codemirror": ">=6.0.0",
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.18.6",
+ "@uiw/react-codemirror": "^4.19.11",
+ "@codemirror/merge": "^6.0.1"
+ },
+ "keywords": [
+ "react",
+ "codemirror",
+ "codemirror6",
+ "react-codemirror",
+ "editor",
+ "syntax",
+ "ide",
+ "code"
+ ],
+ "jest": {
+ "coverageReporters": [
+ "lcov",
+ "json-summary"
+ ]
+ }
+}
diff --git a/merge/src/Internal.tsx b/merge/src/Internal.tsx
new file mode 100644
index 000000000..c91b10a4f
--- /dev/null
+++ b/merge/src/Internal.tsx
@@ -0,0 +1,37 @@
+import React, { useEffect, useRef } from 'react';
+import { MergeView } from '@codemirror/merge';
+import { useStore } from './store';
+
+export interface ReactCodeMirrorMergeInternalProps extends React.LiHTMLAttributes {}
+
+export const Internal = React.forwardRef(
+ (props: ReactCodeMirrorMergeInternalProps, ref?: React.ForwardedRef) => {
+ const { className, children } = props;
+ const { modified, original, view, dispatch } = useStore();
+ const editor = useRef(null);
+
+ useEffect(() => {
+ if (!view && editor.current && original && modified) {
+ const viewDefault = new MergeView({
+ a: original,
+ b: modified,
+ parent: editor.current,
+ });
+ dispatch && dispatch({ view: viewDefault });
+ }
+ }, [editor.current, original, modified, view]);
+
+ useEffect(() => {
+ return () => {
+ view && view.destroy();
+ };
+ }, []);
+
+ const defaultClassNames = 'cm-merge-theme';
+ return (
+
+ {children}
+
+ );
+ },
+);
diff --git a/merge/src/Modified.tsx b/merge/src/Modified.tsx
new file mode 100644
index 000000000..d9afbc39e
--- /dev/null
+++ b/merge/src/Modified.tsx
@@ -0,0 +1,34 @@
+import { useEffect } from 'react';
+import { EditorStateConfig, Extension } from '@codemirror/state';
+import { getDefaultExtensions } from '@uiw/react-codemirror';
+import { useStore } from './store';
+
+export interface ModifiedProps extends EditorStateConfig {
+ value?: EditorStateConfig['doc'];
+ extensions?: Extension[];
+}
+
+export const Modified = (props: ModifiedProps): JSX.Element | null => {
+ const { extensions = [] } = props;
+ const { modified, view, dispatch } = useStore();
+ const defaultExtensions = getDefaultExtensions();
+ useEffect(() => {
+ const data: EditorStateConfig = { extensions: [...defaultExtensions, ...extensions] };
+ if (modified?.doc !== props.value && view) {
+ data.doc = props.value;
+ dispatch!({ modified: { ...modified, ...data } });
+ const modifiedDoc = view?.b.state.doc.toString();
+ if (modifiedDoc !== props.value) {
+ view.b.dispatch({
+ changes: { from: 0, to: (modifiedDoc || '').length, insert: props.value || '' },
+ });
+ }
+ }
+ if (modified?.selection !== props.selection) {
+ data.selection = props.selection;
+ dispatch!({ modified: { ...modified, ...data } });
+ }
+ }, [props.value, props.selection, view]);
+
+ return null;
+};
diff --git a/merge/src/Original.tsx b/merge/src/Original.tsx
new file mode 100644
index 000000000..25b514489
--- /dev/null
+++ b/merge/src/Original.tsx
@@ -0,0 +1,34 @@
+import { useEffect } from 'react';
+import { EditorStateConfig, Extension } from '@codemirror/state';
+import { useStore } from './store';
+import { getDefaultExtensions } from '@uiw/react-codemirror';
+
+export interface OriginalProps extends EditorStateConfig {
+ value?: EditorStateConfig['doc'];
+ extensions?: Extension[];
+}
+
+export const Original = (props: OriginalProps): JSX.Element | null => {
+ const { extensions = [] } = props;
+ const { original, view, dispatch } = useStore();
+ const defaultExtensions = getDefaultExtensions();
+ useEffect(() => {
+ const data: EditorStateConfig = { extensions: [...defaultExtensions, ...extensions] };
+ if (original?.doc !== props.value && view) {
+ data.doc = props.value;
+ dispatch!({ original: { ...original, ...data } });
+ const originalDoc = view?.a.state.doc.toString();
+ if (originalDoc !== props.value) {
+ view?.a.dispatch({
+ changes: { from: 0, to: (originalDoc || '').length, insert: props.value || '' },
+ });
+ }
+ }
+ if (original?.selection !== props.selection) {
+ data.selection = props.selection;
+ dispatch!({ original: { ...original, ...data } });
+ }
+ }, [props.value, props.selection, view]);
+
+ return null;
+};
diff --git a/merge/src/index.tsx b/merge/src/index.tsx
new file mode 100644
index 000000000..e5302c4cf
--- /dev/null
+++ b/merge/src/index.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { Original } from './Original';
+import { Modified } from './Modified';
+import { Internal } from './Internal';
+import { Provider } from './store';
+
+export interface ReactCodeMirrorMergeProps extends React.LiHTMLAttributes {}
+
+const InternalCodeMirror = (props: ReactCodeMirrorMergeProps, ref?: React.ForwardedRef) => {
+ return (
+
+
+
+ );
+};
+
+type CodeMirrorComponent = React.FC> & {
+ Original: typeof Original;
+ Modified: typeof Modified;
+};
+
+const ReactCodeMirrorMerge: CodeMirrorComponent = React.forwardRef(
+ InternalCodeMirror,
+) as unknown as CodeMirrorComponent;
+
+ReactCodeMirrorMerge.Original = Original;
+ReactCodeMirrorMerge.Modified = Modified;
+ReactCodeMirrorMerge.displayName = 'CodeMirrorMerge';
+
+export default ReactCodeMirrorMerge;
diff --git a/merge/src/store.tsx b/merge/src/store.tsx
new file mode 100644
index 000000000..913838279
--- /dev/null
+++ b/merge/src/store.tsx
@@ -0,0 +1,38 @@
+import React, { PropsWithChildren, createContext, useContext, useReducer } from 'react';
+import { EditorStateConfig } from '@codemirror/state';
+import { MergeView } from '@codemirror/merge';
+
+export interface StoreContextValue extends InitialState {
+ dispatch?: React.Dispatch;
+}
+
+export interface InitialState {
+ modified?: EditorStateConfig;
+ original?: EditorStateConfig;
+ view?: MergeView;
+}
+
+export const initialState: InitialState = {
+ modified: { doc: '' },
+ original: { doc: '' },
+};
+
+export const Context = createContext(initialState);
+
+export function reducer(state: InitialState, action: InitialState): InitialState {
+ return {
+ ...state,
+ ...action,
+ modified: { ...state.modified, ...action.modified },
+ original: { ...state.original, ...action.original },
+ };
+}
+
+export const useStore = () => {
+ return useContext(Context);
+};
+
+export const Provider: React.FC> = ({ children }) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ return {children};
+};
diff --git a/merge/tsconfig.json b/merge/tsconfig.json
new file mode 100644
index 000000000..50face246
--- /dev/null
+++ b/merge/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig",
+ "include": ["src"],
+ "compilerOptions": {
+ "outDir": "cjs",
+ "baseUrl": "."
+ }
+}
diff --git a/package.json b/package.json
index 6aaeb10d1..f9da0e174 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
- "build": "lerna exec --scope @uiw/* --ignore www -- npm run build",
+ "build": "lerna exec --scope @uiw/* --scope react-codemirror-merge --ignore www -- npm run build",
"⬇️⬇️⬇️⬇️⬇️ package ⬇️⬇️⬇️⬇️⬇️": "▼▼▼▼▼ package ▼▼▼▼▼",
"watch": "npm run-script watch --workspace @uiw/react-codemirror",
"bundle": "npm run-script bundle --workspace @uiw/react-codemirror",
@@ -18,6 +18,7 @@
"themes/**",
"extensions/**",
"core",
+ "merge",
"www"
],
"engines": {
diff --git a/www/package.json b/www/package.json
index a38f2ffac..c52ba770b 100644
--- a/www/package.json
+++ b/www/package.json
@@ -79,6 +79,7 @@
"code-example": "^3.3.6",
"markdown-react-code-preview-loader": "^2.1.2",
"react": "~18.2.0",
+ "react-codemirror-merge": "0.0.1",
"react-code-preview-layout": "^2.0.5",
"react-dom": "~18.2.0",
"react-router-dom": "^6.3.0",
diff --git a/www/src/components/NavMenus.tsx b/www/src/components/NavMenus.tsx
index ad047f22b..a8ab4e2b4 100644
--- a/www/src/components/NavMenus.tsx
+++ b/www/src/components/NavMenus.tsx
@@ -36,6 +36,7 @@ export const NavMenus = () => {
Home
Extensions
+ Merge
Themes
Themes Editor
Theme Doc
diff --git a/www/src/index.tsx b/www/src/index.tsx
index 84042b2b3..cfb050f7f 100644
--- a/www/src/index.tsx
+++ b/www/src/index.tsx
@@ -7,6 +7,7 @@ import { ThemeEditor } from './pages/theme/editor';
import { ThemeLayout } from './pages/theme';
import { ThemesHome } from './pages/theme/home';
import { ThemeDoc } from './pages/theme/docs';
+import { MergeDoc, MergeLayout } from './pages/merge';
import { ThemeOkaidia } from './pages/theme/themes';
import { ExtensionsLayout } from './pages/extensions';
import { EventsDoc } from './pages/extensions/events';
@@ -73,6 +74,9 @@ root.render(
} />
} />
+ }>
+ } />
+
}>
} />
} />
diff --git a/www/src/pages/merge/Example.tsx b/www/src/pages/merge/Example.tsx
new file mode 100644
index 000000000..ccd294e80
--- /dev/null
+++ b/www/src/pages/merge/Example.tsx
@@ -0,0 +1,23 @@
+import CodeMirrorMerge from 'react-codemirror-merge';
+import { EditorView } from 'codemirror';
+import { EditorState } from '@codemirror/state';
+
+const Original = CodeMirrorMerge.Original;
+const Modified = CodeMirrorMerge.Modified;
+let doc = `one
+two
+three
+four
+five`;
+
+export const Example = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/www/src/pages/merge/data.tsx b/www/src/pages/merge/data.tsx
new file mode 100644
index 000000000..769d128ba
--- /dev/null
+++ b/www/src/pages/merge/data.tsx
@@ -0,0 +1,5 @@
+import mergeMd from 'react-codemirror-merge/README.md';
+
+export const mdSource = {
+ document: mergeMd.source,
+};
diff --git a/www/src/pages/merge/index.tsx b/www/src/pages/merge/index.tsx
new file mode 100644
index 000000000..77340e477
--- /dev/null
+++ b/www/src/pages/merge/index.tsx
@@ -0,0 +1,51 @@
+import { Outlet, useLocation } from 'react-router-dom';
+import { useEffect } from 'react';
+import MarkdownPreview from '@uiw/react-markdown-preview';
+import BackToUp from '@uiw/react-back-to-top';
+import { Example } from './Example';
+import { Header, Warpper } from '../../components/Header';
+import { Sider } from '../theme/editor';
+import { Document } from '../extensions/Document';
+import { PageWarpper } from '../extensions';
+import { Content } from '../theme';
+import { MenuItem } from '../theme/themes/SiderMenus';
+import { mdSource } from './data';
+
+export const MergeDoc = () => {
+ return (
+
+
+
+ Top
+
+ );
+};
+
+export const MergeLayout = () => {
+ const location = useLocation();
+ useEffect(() => {
+ document.title = `${location.pathname.split('/').join(' ')} for CodeMirror 6`;
+ }, [location]);
+ return (
+
+
+
+
+ {Object.keys(mdSource).map((name, key) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};