How to make certain ranges readonly in CodeMirror6

Hello, I’ve been trying to create an editor where the first line and last line are readonly. Is there a way where I can mark certain ranges as readonly in CodeMirror6?

The recommended way to do this is to create a transaction filter that stops transactions which affect those ranges.

3 Likes

I’ve been able to do this with a changeFilter. Issue I have now is that inserting characters before the readonly range doesn’t preserve the readonly range location (readonly ranged is fixed in place instead of moving).

I’ve been able to apply the underline mark to a range. I’d like to combine the two, so any underlined range becomes readonly. Is there a prop somewhere that allows me to check if the current selection is part of a marked range? Haven’t been able to find that anywhere so far.

@marijn Thank you so much for CodeMirror (6 especially). It’s really great on mobile. I’m sure that wasn’t easy. CM6 provides a lot of great lower level primitives. Means steeper learning curve but also much more powerful. For me the tradeoff is worth it, so thank you!

Alright… I have it 90% working with this code…

I’m using the underline decorator example. Then for any transaction I check tr.changes.touchesRange() and compare against the decorator ranges. If there’s overlap I block the transaction (thus getting read-only ranges).

This works really well except for a few problems:

  1. If I delete an entire readOnly range tr.changes.touchesRange() returns false and so it just deletes it. From the docs I assume it should return cover in this case. Did I misinterpret that?
  2. For the life of me I can’t figure out how to see deletes show up in tr.changes. Is there a setting to enable that?
  3. If I apply this to a line with a single char (closing bracket }) it doesn’t protect the character, but the next (empty) line. I’m guessing the line-ending/line-break is somehow interfering.
  4. It took me a LONG time to write the iterating code below. Is there an easier way to iterate and compare changeRanges vs stored decorator ranges?

Any thoughts or ideas?

function readOnlyTransactionFilter() {
  return EditorState.transactionFilter.of((tr) => {
    if (tr.docChanged && !tr.annotation(Transaction.remote)) {
      const newTr = [];

      var readonlyRangeSet = tr.state.field(underlineField, false);

      var readonlyRanges = readonlyRangeSet?.chunk || [];

      // bail early if nothing was marked as read-only
      if (!readonlyRangeSet?.chunk) {
        return tr;
      }

      for (var i=0; i<readonlyRanges.length; i++) {
        var chunk = readonlyRanges[i];
        var offset = readonlyRangeSet?.chunkPos[i];
      
        for (var j=0; j<chunk.from.length; j++) {

          var start = chunk.from[j] + offset;
          var end = chunk.to[j] + offset;

          var readonlyRangesAffected = tr.changes.touchesRange(start, end);

          if(readonlyRangesAffected) return [];
        }
      }
   return tr;
}

Definitely don’t use undocumented properties like RangeSet.chunk, those may break on any release. Also, you’ll want to use a consistent coordinate system, so using the read-only ranges from tr.startState and the original-document coordinates in the change set seems safer. Something like this (untested) might work:

function readOnlyTransactionFilter() {
  return EditorState.transactionFilter.of((tr) => {
    let readonlyRangeSet = tr.startState.field(underlineField, false)
    if (readonlyRangeSet && tr.docChanged && !tr.annotation(Transaction.remote)) {
      let block = false
      tr.changes.iterChangedRanges((chFrom, chTo) => {
        readonlyRangeSet.between(chFrom, chTo, (roFrom, roTo) => {
          if (chTo > roFrom && chFrom < roTo) block = true
        })
      })
      if (block) return []
    }
    return tr
  }
}
1 Like

I’m seeing that for a covered delete operation the stateFields seem to get updated immediately. I’ll see if I can use the prior state for those numbers instead.

This worked BEAUTIFULLY! Thank you! I think the only change I had to make was to add a closing paren ) at the end of the 2nd-to-last line.

No rush, but I’m curious, what does a vs b represent here in fromA vs fromB (see image below)? I wasn’t certain which of these to use earlier. It looks like you’re dropping fromB and toB in the code above.
Screen Shot 2022-02-11 at 1.13.36 AM

1 Like

fromA and toA are the range in the original document, which is replaced by the fromB to toB range in the new document. (So for an insertion, fromA == toA, and for a deletion, fromB == toB).

2 Likes

Ah fascinating! I’ll need to experiment and see if I can use this to disallow either insertion or deletion only.

Thanks!

I just released codemirror-readonly-ranges extension that easy allow to work with read-only ranges on CodeMirror6.
For anyone who is interested on the solution:

package: https://www.npmjs.com/package/codemirror-readonly-ranges

full documentation: https://andrebnassis.github.io/codemirror-readonly-ranges

4 Likes

@andrebnassis Thank you so much!! This is exactly what I needed :slight_smile:

1 Like