Detect cursor motion into atomicRanges widget

In the docs on atomicRanges there is facet extension used to skip atoms. I was wondering, is it possible to detect the cursor movement into the replacement atomic decoration and send an event to the corresponding WidgetType?

I guess I need to apply a custom transaction filter to stop the cursor movement and fire an event on a widget. So I check the source code of atomicRanges to have an idea how to do it

function skipAtoms(view, oldPos, pos) {
    let atoms = view.state.facet(atomicRanges).map(f => f(view));
    for (;;) {
        let moved = false;
        for (let set of atoms) {
            set.between(pos.from - 1, pos.from + 1, (from, to, value) => {
                if (pos.from > from && pos.from < to) {
                    pos = oldPos.head > pos.from ? EditorSelection.cursor(from, 1) : EditorSelection.cursor(to, -1);
                    moved = true;
                }
            });
        }
        if (!moved)
            return pos;
    }
}

It looks like that an atom object contains only ranges of symbols, which cover atoms, but they have no direct references to the instances of a WidgetType placed on the corresponding range. Or where should I look for that?

Thanks.

No, atomicRanges can’t do that, but instead of using that you could define a transaction filter that, when the selection ends up inside one of your ranges, moves it as appropriate and adds some effect that makes your extension update the appropriate widget (widgets are stateless, so you don’t really send events to them, but rather you replace them with a different instance).

I tried transactionFilter it works by its own flawlessly. However, I don’t know how to prioritize it over atomicRange features.

I managed to detect the cursor going into an atomic range decoration, but it can only detect 1 character before/after the actual chunk. It seems atomic range has a priority in transactions and alters the transaction before my filter, which leads to a jump over the widget position and then my filter is useless.

Here is an example

Here is an example

const ref = {}; //just a reference to atomicRange decorations

return [
    //filter
    EditorState.transactionFilter.of((tr) => {
      if (tr.selection && ref.placeholder) {
        if (tr.selection.ranges[0].from == tr.selection.ranges[0].to && ref.placeholder.placeholder.chunk[0]) {
          
          if (findRegion(
ref.placeholder.placeholder.chunkPos[0],  //chunk offset
ref.placeholder.placeholder.chunk[0].from, 
ref.placeholder.placeholder.chunk[0].to, 
tr.selection.ranges[0].from
) > -1) {

            return false;
          }
        }
      }

      return tr
    }),

    //decoration
    placeholder(ref)
]

I found in source code, that function skipAtoms already checks chunks

function skipAtoms(view, oldPos, pos) {
    let atoms = view.state.facet(atomicRanges).map(f => f(view));
    for (;;) {
        let moved = false;
        for (let set of atoms) {
            set.between(pos.from - 1, pos.from + 1, (from, to, value) => {
                if (pos.from > from && pos.from < to) {
                    pos = oldPos.head > pos.from ? EditorSelection.cursor(from, 1) : EditorSelection.cursor(to, -1);
                    moved = true;

                    console.warn(value.widget); // HERE
                }
            });
        }
        if (!moved)
            return pos;
    }
}

It would be great to have somewhat a callback function from this or that atomicRange instance, which by the default returns true and allows alter the transaction. Then, anyone could define such function for a widget and avoid extra computations when checking borders in transaction filters (second time).

In the given example an access to a widget is possible via value.widget

UPD: Something like this could extend the features of atomicRange

function skipAtoms(view, oldPos, pos) {
    let atoms = view.state.facet(atomicRanges).map(f => f(view));
    for (;;) {
        let moved = false;
        for (let set of atoms) {
            set.between(pos.from - 1, pos.from + 1, (from, to, value) => {
                if (pos.from > from && pos.from < to) {
                    pos = oldPos.head > pos.from ? EditorSelection.cursor(from, 1) : EditorSelection.cursor(to, -1);
                    moved = true;

                    if (value.widget.skipPosition) { //HERE
                        pos = value.widget.skipPosition(pos, oldPos)
                    }                    
                }
            });
        }

        if (!moved)
            return pos;
    }
}

selection-ezgif.com-video-to-gif-converter

This trick works great. However, I am feeling guilty for modifying source code of cursor.ts

1 Like

If you don’t want the atomic range treatment, it seems you could just avoid marking your ranges as atomic, no?

After two days, I have realized, I probably need to implement a custom version of atomicRanges where one could control this behavior, then there is no need in making more complicated version of atomic ranges.

Anyway, thank you again for creating CM6! This is amazing how many complex things it can handle with no performance issues.

Topic closed