diff --git a/.yarn/versions/45a55aa9.yml b/.yarn/versions/45a55aa9.yml
new file mode 100644
index 0000000..548c656
--- /dev/null
+++ b/.yarn/versions/45a55aa9.yml
@@ -0,0 +1,2 @@
+releases:
+ "@handlewithcare/react-codemirror": minor
diff --git a/README.md b/README.md
index 9d222d9..f2ef2c6 100644
--- a/README.md
+++ b/README.md
@@ -21,9 +21,11 @@ yarn add @handlewithcare/react-codemirror
- [Overview](#overview)
- [Usage](#usage)
+ - [Dynamic extensions](#dynamic-extensions)
- [API](#api)
- [`CodeMirror`](#codemirror)
- [`CodeMirrorEditor`](#codemirroreditor)
+ - [`react`](#react)
- [`useEditorState`](#useeditorstate)
- [`useCompartment`](#usecompartment)
- [`useEditorEventCallback`](#useeditoreventcallback)
@@ -42,15 +44,17 @@ may support React-based widgets and tooltips.
## Usage
-To get started, render the `CodeMirror` and `CodeMirrorEditor` components:
+To get started, render the `CodeMirror` and `CodeMirrorEditor` components, and
+add the `react` extension to your EditorState:
```tsx
import { EditorState, type Transaction } from "@codemirror/state";
-import { CodeMirror, CodeMirrorEditor } from "@handlewithcare/react-codemirror";
+import { CodeMirror, CodeMirrorEditor, react } from "@handlewithcare/react-codemirror";
import { basicSetup } from "codemirror";
import React, { StrictMode, useCallback, useState } from "react";
-const editorState = EditorState.create({ doc: "", basicSetup });
+// NOTE: You must also add the `react` extension to your EditorState!
+const editorState = EditorState.create({ doc: "", [basicSetup, react] });
function CodeEditor() {
const [state, setState] = useState(editorState);
@@ -75,6 +79,11 @@ The `CodeMirrorEditor` is where the actual CodeMirror editor will be
instantiated. It can be nested anywhere as a descendant of the `CodeMirror`
component.
+The `react` extension is necessary for ensuring that the React state stays in
+sync with the CodeMirror EditorState.
+
+### Dynamic extensions
+
The `useReconfigure` hook can be used to configure dynamic CodeMirror
extensions. Here’s an example, using a simple theme switcher:
@@ -269,6 +278,15 @@ function CodeEditor() {
}
```
+### `react`
+
+```ts
+type Extension;
+```
+
+A CodeMirror extension that allows react-codemirror to keep React state in sync
+with CodeMirror state.
+
### `useEditorState`
```ts
diff --git a/demo/main.tsx b/demo/main.tsx
index df8f95d..d1f2edb 100644
--- a/demo/main.tsx
+++ b/demo/main.tsx
@@ -8,6 +8,7 @@ import { createRoot } from "react-dom/client";
import {
CodeMirror,
CodeMirrorEditor,
+ react,
useEditorState,
useReconfigure,
} from "../src/index.js";
@@ -17,10 +18,93 @@ const themeCompartment = new Compartment();
const extensions = [
basicSetup,
javascript({ jsx: true, typescript: true }),
+ react,
themeCompartment.of(oneDark),
];
-const editorState = EditorState.create({ doc: `const a = "a"`, extensions });
+const editorState = EditorState.create({
+ doc: `/**
+ * @fileoverview
+ * This is the code that runs this demo!
+ */
+
+import { javascript } from "@codemirror/lang-javascript";
+import { Compartment, EditorState, type Transaction } from "@codemirror/state";
+import { oneDark } from "@codemirror/theme-one-dark";
+import { basicSetup } from "codemirror";
+import React, { StrictMode, useCallback, useState } from "react";
+import { createRoot } from "react-dom/client";
+
+import {
+ CodeMirror,
+ CodeMirrorEditor,
+ react,
+ useEditorState,
+ useReconfigure,
+} from "@handlewithcare/react-codemirror";
+
+const themeCompartment = new Compartment();
+
+const extensions = [
+ basicSetup,
+ javascript({ jsx: true, typescript: true }),
+ react,
+ themeCompartment.of(oneDark),
+];
+
+const editorState = EditorState.create({ doc: \`const a = "a"\`, extensions });
+
+function ThemePicker() {
+ const state = useEditorState();
+ const theme = themeCompartment.get(state);
+ const dark = theme === oneDark;
+ const reconfigureTheme = useReconfigure(themeCompartment);
+
+ return (
+
+ );
+}
+
+function DemoEditor() {
+ const [state, setState] = useState(editorState);
+
+ const dispatchTransactions = useCallback((trs: readonly Transaction[]) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ setState(trs[trs.length - 1]!.state);
+ }, []);
+
+ return (
+
+ React CodeMirror demo
+
+
+
+
+
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+const root = createRoot(document.getElementById("root")!);
+
+root.render(
+
+
+ ,
+);
+`,
+ extensions,
+});
function ThemePicker() {
const state = useEditorState();
diff --git a/package.json b/package.json
index ce26160..534ad95 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
},
"devDependencies": {
"@codemirror/lang-javascript": "^6.2.3",
- "@codemirror/merge": "^6.9.0",
+ "@codemirror/lang-json": "^6.0.2",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.36.3",
@@ -76,7 +76,6 @@
},
"packageManager": "yarn@4.6.0",
"peerDependencies": {
- "@codemirror/merge": "*",
"@codemirror/state": "*",
"@codemirror/view": "*",
"react": ">=17 <=19",
diff --git a/src/extensions/tracking.ts b/src/extensions/tracking.ts
new file mode 100644
index 0000000..8e266c4
--- /dev/null
+++ b/src/extensions/tracking.ts
@@ -0,0 +1,10 @@
+import { StateField, type Transaction } from "@codemirror/state";
+
+export const tracking = StateField.define({
+ create() {
+ return [];
+ },
+ update(value, tr) {
+ return [...value, tr];
+ },
+});
diff --git a/src/hooks/useEditor.ts b/src/hooks/useEditor.ts
index 864d055..6dd30ac 100644
--- a/src/hooks/useEditor.ts
+++ b/src/hooks/useEditor.ts
@@ -1,7 +1,8 @@
-import { diff } from "@codemirror/merge";
import { EditorState, type Transaction } from "@codemirror/state";
import { EditorView, type EditorViewConfig } from "@codemirror/view";
-import { useCallback, useLayoutEffect, useState } from "react";
+import { useCallback, useLayoutEffect, useRef, useState } from "react";
+
+import { tracking } from "../extensions/tracking.js";
export type UseViewOptions = Omit & {
defaultState?: EditorState;
@@ -40,6 +41,8 @@ export function useEditor(
const [_state, setState] = useState(defaultState);
const state = options.state ?? _state;
+ const seen = useRef>(new Set());
+
const dispatchTransactions = useCallback(
function dispatchTransactions(
trs: readonly Transaction[],
@@ -86,36 +89,12 @@ export function useEditor(
return;
}
- // Fully replace the state after reconfiguration. We don't have
- // access to the effects that were used to produce the new state
- // config, so we have to just do a brute force replace
- // @ts-expect-error Internal properties
- if (view.state.config !== state.config) {
- view.setState(state);
- return;
- }
+ const trs = state.field(tracking);
+ const newTrs = trs.filter((tr) => !seen.current.has(tr));
- if (
- !view.state.doc.eq(state.doc) ||
- !view.state.selection.eq(state.selection)
- ) {
- // This can take a few milliseconds on a large document,
- // but it prevents codemirror from having to redo syntax
- // highlighting, etc, from scratch on each update
- const current = view.state.doc.toString();
- const incoming = state.doc.toString();
- const diffed = diff(current, incoming);
-
- view.update([
- view.state.update({
- changes: diffed.map((change) => ({
- from: change.fromA,
- to: change.toA,
- insert: incoming.slice(change.fromB, change.toB),
- })),
- selection: state.selection,
- }),
- ]);
+ if (newTrs.length) {
+ view.update(newTrs);
+ newTrs.forEach((tr) => seen.current.add(tr));
}
});
diff --git a/src/index.ts b/src/index.ts
index 136af7d..666dc51 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,3 +6,6 @@ export { useEditorEffect } from "./hooks/useEditorEffect.js";
export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js";
export { useEditorState } from "./hooks/useEditorState.js";
export { useReconfigure } from "./hooks/useReconfigure.js";
+import { tracking } from "./extensions/tracking.js";
+
+export const react = [tracking];
diff --git a/yarn.lock b/yarn.lock
index e8ebee4..c93d54b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -260,6 +260,16 @@ __metadata:
languageName: node
linkType: hard
+"@codemirror/lang-json@npm:^6.0.2":
+ version: 6.0.2
+ resolution: "@codemirror/lang-json@npm:6.0.2"
+ dependencies:
+ "@codemirror/language": "npm:^6.0.0"
+ "@lezer/json": "npm:^1.0.0"
+ checksum: 10/a8e57ad5c6cc53edd0d8bce4c26fa189dd5a95c371e72d32957d4a5c857f814761d9dfab81361f8e8ae9da7bcf657e7e1343f39694f9cffb1c144dd3ef7092a0
+ languageName: node
+ linkType: hard
+
"@codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.6.0":
version: 6.10.8
resolution: "@codemirror/language@npm:6.10.8"
@@ -285,19 +295,6 @@ __metadata:
languageName: node
linkType: hard
-"@codemirror/merge@npm:^6.9.0":
- version: 6.9.0
- resolution: "@codemirror/merge@npm:6.9.0"
- dependencies:
- "@codemirror/language": "npm:^6.0.0"
- "@codemirror/state": "npm:^6.0.0"
- "@codemirror/view": "npm:^6.17.0"
- "@lezer/highlight": "npm:^1.0.0"
- style-mod: "npm:^4.1.0"
- checksum: 10/16d4f745c113e93a4da120a4f854743451c4dce9ba46dcba1a53926714a1f8396742b570764ce044a3c647de77c91a24c7d05af3b3ea4620eb516fe466a3bc5e
- languageName: node
- linkType: hard
-
"@codemirror/search@npm:^6.0.0":
version: 6.5.10
resolution: "@codemirror/search@npm:6.5.10"
@@ -612,7 +609,7 @@ __metadata:
resolution: "@handlewithcare/react-codemirror@workspace:."
dependencies:
"@codemirror/lang-javascript": "npm:^6.2.3"
- "@codemirror/merge": "npm:^6.9.0"
+ "@codemirror/lang-json": "npm:^6.0.2"
"@codemirror/state": "npm:^6.5.2"
"@codemirror/theme-one-dark": "npm:^6.1.2"
"@codemirror/view": "npm:^6.36.3"
@@ -648,7 +645,6 @@ __metadata:
vite: "npm:^6.2.0"
vitest: "npm:^3.0.7"
peerDependencies:
- "@codemirror/merge": "*"
"@codemirror/state": "*"
"@codemirror/view": "*"
react: ">=17 <=19"
@@ -786,6 +782,17 @@ __metadata:
languageName: node
linkType: hard
+"@lezer/json@npm:^1.0.0":
+ version: 1.0.3
+ resolution: "@lezer/json@npm:1.0.3"
+ dependencies:
+ "@lezer/common": "npm:^1.2.0"
+ "@lezer/highlight": "npm:^1.0.0"
+ "@lezer/lr": "npm:^1.0.0"
+ checksum: 10/48e7b945fdfa2b5b6f862e27bc31f3991cba93f18df7fed0059b25f119b64dedd50bbc709d279e16e2b3eee10e7758d7d80c6d98d21bc15c284809d268837897
+ languageName: node
+ linkType: hard
+
"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.3.0":
version: 1.4.2
resolution: "@lezer/lr@npm:1.4.2"