uiwjs / react-codemirror

CodeMirror 6 component for React. @codemirror https://uiwjs.github.io/react-codemirror/
https://uiwjs.github.io/react-codemirror/
MIT License
1.69k stars 135 forks source link

out of sync view state / uncatchable errors #531

Open felixroos opened 1 year ago

felixroos commented 1 year ago

in some cases, the view state seems to be out of sync. I am updating Decorations in an animation frame, and it can happen that the view dispatches from an old state, which can create errors. these errors also don't seem to be catchable with all I've tried.

I've made a minimal repro here:

https://codesandbox.io/s/react-codemirror-range-error-forked-pcpwtl?file=/src/App.js

I also wrote a post in the codemirror forum: https://discuss.codemirror.net/t/uncatchable-out-of-range-errors-when-document-changes/4864

This was all some time ago and I already found a workaround but today I ran into a similar problem again.

Not sure what to do about this..

jaywcjlove commented 1 year ago

@felixroos I reproduced this error using codemirror, maybe the problem has nothing to do with react.

https://codemirror.net/try/?c=aW1wb3J0IHtiYXNpY1NldHVwLCBFZGl0b3JWaWV3fSBmcm9tICJjb2RlbWlycm9yIgppbXBvcnQge2phdmFzY3JpcHR9IGZyb20gIkBjb2RlbWlycm9yL2xhbmctamF2YXNjcmlwdCIKaW1wb3J0IHsgU3RhdGVGaWVsZCwgU3RhdGVFZmZlY3QgfSBmcm9tICJAY29kZW1pcnJvci9zdGF0ZSI7CmltcG9ydCB7IERlY29yYXRpb24gfSBmcm9tICJAY29kZW1pcnJvci92aWV3IjsKCmNvbnN0IHNldEhpZ2hsaWdodHMgPSBTdGF0ZUVmZmVjdC5kZWZpbmUoKTsKY29uc3QgaGlnaGxpZ2h0RmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShoaWdobGlnaHRzLCB0cikgewogICAgZm9yIChsZXQgZSBvZiB0ci5lZmZlY3RzKSB7CiAgICAgIGNvbnN0IGNoYXIgPSBlLnZhbHVlOwogICAgICBpZiAoZS5pcyhzZXRIaWdobGlnaHRzKSAmJiB0ci5zdGF0ZS5kb2MubGVuZ3RoKSB7CiAgICAgICAgY29uc3QgbWFyayA9IERlY29yYXRpb24ubWFyayh7CiAgICAgICAgICBhdHRyaWJ1dGVzOiB7IHN0eWxlOiBgb3V0bGluZTogMnB4IHNvbGlkIGJsdWVgIH0KICAgICAgICB9KS5yYW5nZShjaGFyLCBjaGFyICsgMSk7CiAgICAgICAgaGlnaGxpZ2h0cyA9IERlY29yYXRpb24uc2V0KFttYXJrXSk7CiAgICAgIH0KICAgIH0KICAgIHJldHVybiBoaWdobGlnaHRzOwogIH0sCiAgcHJvdmlkZTogKGYpID0+IEVkaXRvclZpZXcuZGVjb3JhdGlvbnMuZnJvbShmKQp9KTsKCgp2YXIgdmlldyA9IG5ldyBFZGl0b3JWaWV3KHsKICBkb2M6ICJjb25zb2xlLmxvZygnaGVsbG8nKVxuIiwKICBleHRlbnNpb25zOiBbYmFzaWNTZXR1cCwgamF2YXNjcmlwdCgpLCBoaWdobGlnaHRGaWVsZF0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5Cn0pCgoKdmFyIGZyYW1lID0gcmVxdWVzdEFuaW1hdGlvbkZyYW1lKHVwZGF0ZUhpZ2hsaWdodHMpOwpmdW5jdGlvbiB1cGRhdGVIaWdobGlnaHRzKCkgewogIHRyeSB7CiAgICBjb25zdCBjaGFyID0gTWF0aC5yb3VuZChNYXRoLnJhbmRvbSgpICogdmlldy5zdGF0ZS5kb2MubGVuZ3RoIC0gMSk7CiAgICB2aWV3LmRpc3BhdGNoKHsgZWZmZWN0czogc2V0SGlnaGxpZ2h0cy5vZihjaGFyKSB9KTsgLy8gaGlnaGxpZ2h0IGFsbCBzdGlsbCBhY3RpdmUgKyBuZXcgYWN0aXZlIGhhcHMKICAgIGZyYW1lID0gcmVxdWVzdEFuaW1hdGlvbkZyYW1lKHVwZGF0ZUhpZ2hsaWdodHMpOwogIH0gY2F0Y2ggKGVycikgewogICAgLy8gZXZlbiB0aG91Z2ggd2UgY2F0Y2ggZXJyb3JzIGhlcmUsCiAgICAvLyB0aGUgcmFuZ2UgZXJyb3Igc3RpbGwgY3Jhc2hlcwogICAgY29uc29sZS5sb2coImVyciIsIGVycik7CiAgfQp9

jaywcjlove commented 1 year ago

React Example.

import CodeMirror from "@uiw/react-codemirror";
import { useState, useEffect, useCallback } from "react";
import { StateField, StateEffect } from "@codemirror/state";
import { EditorView, Decoration } from "@codemirror/view";

export const setHighlights = StateEffect.define();
const highlightField = StateField.define({
  create() {
    return Decoration.none;
  },
  update(highlights, tr) {
    for (let e of tr.effects) {
      const char = e.value;
      if (e.is(setHighlights) && tr.state.doc.length) {
        const mark = Decoration.mark({
          attributes: { style: `outline: 2px solid blue` }
        }).range(char, char + 1);
        highlights = Decoration.set([mark]);
      }
    }
    return highlights;
  },
  provide: (f) => EditorView.decorations.from(f)
});

function useHighlighting(view) {
  useEffect(() => {
    let frame;
    if (view) {
      console.log("start highlighting");
      frame = requestAnimationFrame(updateHighlights);
      function updateHighlights() {
        try {
          const char = Math.round(Math.random() * view.state.doc.length - 1);
          view.dispatch({ effects: setHighlights.of(char) }); // highlight all still active + new active haps
          frame = requestAnimationFrame(updateHighlights);
        } catch (err) {
          // even though we catch errors here,
          // the range error still crashes
          console.log("err", err);
        }
      }
    }
    return () => {
      frame && cancelAnimationFrame(frame);
    };
  }, [view]);
}
const extensions = [highlightField];

export default function App() {
  const [view, setView] = useState();
  const [value, setValue] = useState(
    "delete me with ctrl+a delete to get an error"
  );
  const handleOnChange = useCallback((v) => {
    setValue(v);
  }, []);
  const handleOnCreateEditor = useCallback((v) => {
    setView(v);
  }, []);
  console.log("value", value);
  useHighlighting(view);
  return (
    <div>
      <CodeMirror
        value={value}
        onCreateEditor={handleOnCreateEditor}
        onChange={handleOnChange}
        extensions={extensions}
      />
    </div>
  );
}
import {basicSetup, EditorView} from "codemirror"
import {javascript} from "@codemirror/lang-javascript"
import { StateField, StateEffect } from "@codemirror/state";
import { Decoration } from "@codemirror/view";

const setHighlights = StateEffect.define();
const highlightField = StateField.define({
  create() {
    return Decoration.none;
  },
  update(highlights, tr) {
    for (let e of tr.effects) {
      const char = e.value;
      if (e.is(setHighlights) && tr.state.doc.length) {
        const mark = Decoration.mark({
          attributes: { style: `outline: 2px solid blue` }
        }).range(char, char + 1);
        highlights = Decoration.set([mark]);
      }
    }
    return highlights;
  },
  provide: (f) => EditorView.decorations.from(f)
});

var view = new EditorView({
  doc: "console.log('hello')\n",
  extensions: [basicSetup, javascript(), highlightField],
  parent: document.body
})

var frame = requestAnimationFrame(updateHighlights);
function updateHighlights() {
  try {
    const char = Math.round(Math.random() * view.state.doc.length - 1);
    view.dispatch({ effects: setHighlights.of(char) }); // highlight all still active + new active haps
    frame = requestAnimationFrame(updateHighlights);
  } catch (err) {
    // even though we catch errors here,
    // the range error still crashes
    console.log("err", err);
  }
}