RangeSet.map(tr.changes) does move range if text is right before

Why Range in StateField does not move after RangeSet.map(tr.changes) when symbols are inserted right before the range, e.g.:

  1. Text
    “Lorem ipsum|”
  2. Create range [1, 3]
    “Lorem ipsum|”
  3. Move selector to [1]
    “L|orem ipsum”
  4. Type symbols
    “LAA|orem ipsum”
  5. Range would map to [1, 6] not to [3, 5]

Code is below

import * as cmView from '@codemirror/view'
import * as cmRangeSet from '@codemirror/rangeset'
import * as cmState from "@codemirror/state"

/* CONSTs */
const TAG_PREFIX = '/* A:'
const TAG_POSTFIX = '*/'
const TAG = TAG_PREFIX + '  ' + TAG_POSTFIX

/* EFFECTs and FIELDs */

const effectStart = cmState.StateEffect.define()
const effectAddRange = cmState.StateEffect.define()
const fieldBlocks = cmState.StateField.define({
  create: () => cmRangeSet.RangeSet.empty,
  update: (value, tr) => {
    value = value.map(tr.changes)
    for (let iter = value.iter(); iter.value !== null; iter.next())
      console.log('range:\t', iter.from, ' - ', iter.to)

    let newBlock = tr.effects.find(e => e.is(effectAddRange))
    if (newBlock) {
      value = value.update({
        add: [new cmRangeSet.Range(
          newBlock.value.from,
          newBlock.value.to,
          { type: null, complete: false, params: [] },
        )]
      })
    }
    return value
  },
})

function transactionFilter(tr) {
  let rangesBlocks = tr.startState.field(fieldBlocks)

  // Add new block with comment
  const newBlocksEffects = tr.effects.filter(i =>
    i.is(effectStart) &&
    (rangesBlocks.update({ filter: (from, to) => from <= i.value.head && i.value.head <= to })).size === 0
  )

  const newBlocksTr = newBlocksEffects.map((
    { value }) => ({
      changes: { from: value.head, insert: TAG },
      effects: effectAddRange.of({ from: value.head, to: value.head + TAG.length }),
      selection: cmState.EditorSelection.cursor(value.head + TAG_PREFIX.length + 1)
    })
  )
  return [tr, ...newBlocksTr]
}

/* COMMANDs */

function commandStartAssist(view) {
  const range = view.state.selection.main
  if (view.state.readOnly || !range.empty) return false
  view.dispatch({ effects: effectStart.of({ head: range.head }) })
  return true
}

/* KEYMAPs */
const keymap = cmView.keymap.of([{
  key: 'Ctrl-a',
  preventDefault: true,
  run: commandStartAssist
}])

export const assist = [
  fieldBlocks,
  keymap,
  cmState.EditorState.transactionFilter.of(transactionFilter)
]

Make sure your range values actually inherit from RangeValue, and look into the startSide and endSide properties on that class.

Thanks a lot, that worked!

Best regards,
Ilya Kochik