uncatchable out of range errors when document changes

Hello and thanks for creating this lovely tool!

I am writing a music live coding editor where decoration changes can be dispatched every frame.
When the document gets shorter, it can happen that changes are fired that still receive the old document state, leading to errors like:

Position 281 is out of range for changeset of length 180

When that error happens, the application crashes and gets into an unusable state. It would be really helpful to be able to catch that error and just ignore it, because it is not critical if a highlight is missed.

I created a minimal reproduction here: react-codemirror-range-error - CodeSandbox the error will be thrown whenever the document content is deleted all at once.

I assume the problem is that the document changes between the call to view.dispatch and the actual execution, possibly because requestAnimationFrame is not fired synchronously.

Questions:

  1. Is there a way to hook into codemirrors requestAnimationFrame loop or a way to always get the latest state of the document?
  2. If this is a just a problem with the react component, wouldn’t it still make sense to be able to catch critical errors?

Greetings, Felix

It seems I have fixed it like this… I have to test this with my actual setup to verify that this was the problem…

dispatch happens entirely synchronously. If you can reproduce an issue like this without react and react-codemirror, I’d be interested in debugging that. As it is, I don’t know what magic might be going on inside that React component.

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

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

Your field definition isn’t mapping the decoration set through the transaction’s changes, which will cause it to still have decorations pointing at parts of the document that no longer exist, leading to this error. Add a line like highlights = highlights.map(tr.changes) at the top of the update function to avoid that.

1 Like

It works fine.

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) {
    highlights = highlights.map(tr.changes)
    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);
  }
}

thank you both for the help! We have now managed to use the codemirror API to make the highlights work much better :slight_smile: FYI this is the result: Strudel REPL
extra thanks to @marijn for the incredible work in the JS space! we use both acorn and codemirror in strudel and it’s amazing what you can do with it