Migrating readonly TextMarkers from CodeMirror 5 to 6

I have an old project that I’m breathing some new life into.
It was using CodeMirror 5’s TextMarker API to mark parts of an editor as readonly.

You can see it here: https://github.com/acheronfail/rttw/blob/db42647a9f66299b0b3a7894c5a77a7683db872b/packages/client/src/editor/utils.tsx#L114-L116

Basically, the editor is configured so there’s a prefix (readonly) an editable section in the middle, and a suffix (readonly).

I’m struggling quite a bit to re-implement this in CodeMirror 6, since the API is vastly different. I think I should be using something called atomicRanges? I’m going in circles trying to figure out how to get this working though… Any help would be greatly appreciated.

To block changes in a given part of the document, you’ll have to use changeFilter or transactionFilter in the 6.x API.

Awesome, thanks for pointing me in the right direction.

I’ve managed to get selection working (mostly) fine with this:

EditorState.transactionFilter.of((tr) => {
  const from = prefix.length + 1;
  const to = tr.state.doc.length - suffix.length - 1;

  if (!tr.newSelection.ranges.some((r) => r.from < from || r.to > to)) return tr;

  const selection = EditorSelection.create(
    tr.newSelection.ranges.map((r) => EditorSelection.range(clamp(r.anchor, from, to), clamp(r.head, from, to))),
    tr.newSelection.mainIndex
  );

  return [tr, { selection }];
})

But, there are a few issues:

  • pressing backspace will still delete and eat into my readonly range at the start
  • pressing delete does the same thing

I’m not seeing a clear way for me to detect changes past these boundaries… Unless I’ve missed something obvious?

Oh wait, I think I fixed it by not returning the original Transaction if I detected an out-of-bounds selection.

  // I changed this:
  return [tr, { selection }];
  // to this:
  return [{ selection }];

I’ll test this out and see if it works for corner cases…

Sorry for posting again, but I thought my final solution here could be useful to someone in the future!

This was significantly more difficult than CodeMirror 5, but I got something that’s working enough. Here’s the result:

EditorState.transactionFilter.of((tr) => {
  const from = prefix.length;
  const to = tr.newDoc.length - suffix.length;

  // check any changes are out of bounds
  let oob = false;
  tr.changes.iterChanges(
    (fromA, toA, fromB, toB, inserted) => (oob = oob || fromB < from || toB > to + inserted.length)
  );
  if (oob) return [];

  // check any selections are out of bounds
  const selectionOkay = tr.newSelection.ranges.every((r) => r.from >= from && r.to <= to);
  if (selectionOkay) return tr;

  // create new selection which is in bounds
  const selection = EditorSelection.create(
    tr.newSelection.ranges.map((r) => EditorSelection.range(clamp(r.anchor, from, to), clamp(r.head, from, to))),
    tr.newSelection.mainIndex
  );

  return [{ selection }];
})

There’s one edge-case (that I know of) that this doesn’t handle. Originally I wanted a character of whitespace after the prefix and before the suffix, but that can’t work right now.

Consider the following:

// "I" represents the cursor position
// 
// prefix is "function foo( "
// suffix is " ) {\n  return 1;\n}"
// which means editable range is 0 length right now
function foo( I ) {
  return 1;
}

If a newline is inserted in this position, CodeMirror tries to trim the whitespace before the cursor before adding the newline, which is out of bounds and thus is cancelled. I didn’t find a way to fix this.

Also, when it’s setup like this, moving to the far end of the boundary and entering a newline will also cause the selection to go out of bounds… I’m not sure why.

Either way, I’ve spent way too long already just trying to make some sections readonly :sweat_smile: this is good enough for now.