Inject object into ViewPlugin/MatchDecorator

I define a ViewPlugin below, which adds <span title=""></span> to certain elements of the text. The title needs to be fetched using a helper object. This object should ideally be injected into the ViewPlugin and the corresponding MatchDecorator from the place where EditorState.create() is called, and the plugins are assigned. Is this possible?

let myDecorator = new MatchDecorator({
    regexp: new RegExp('my_regexp', 'gi'),
    decoration: (match) => {
        // Access helper object here to fetch title using match
        let title = this.helper.getTitle(match[1])
        
        return Decoration.mark({
            tagName: 'span',
            attributes: {
                title: title
            }
        })
    }
})

export default ViewPlugin.define(view => ({
    myDecorations: myDecorator.createDeco(view),

    update(u) {
        this.myDecorations = myDecorator.updateDeco(u, this.myDecorations)
    }
}), {
    provide: [
        PluginField.decorations.from(v => v.myDecorations),
    ]
})

The recommended way to do this would be to store your object in a state field, and have a state effect that can update it. That way changes to it are tied into the editor’s general update cycle, and your plugin can check whether the value changed in its update method and update itself accordingly.

Okay, thanks! I failed to figure out how to assign a new StateField containing my object to the current state.

StateField.define() creates a new field. EditorState.field() gets fields from the state, but I’m missing a EditorState.setField() here. Do I need to fire a Transaction with a StateEffect to set the field value? But then StateEffect only takes a value, but not a StateField.

I’m a bit lost tbh. Maybe a little snippet would help.

All state changes must happen through transactions. So your field’s update function reads the effect value to create its new field value, and you dispatch a transaction including that effect.

Okay, I think I’m almost there. Created a lib containing my helper field and effect:

import { StateField, StateEffect } from '@codemirror/state'

const HelperEffect = StateEffect.define()

const helperField = StateField.define({
    create() {
        return null
    },

    update(helper, transaction) {
        transaction.effects.forEach((effect) => {
            if (effect.is(HelperEffect)) {
                return effect.value
            }
        })

        return helper
    }
})

export { helperField, HelpEffect }

I use these to init my state:

let view = new EditorView({
    state: EditorState.create({
        […]
        extensions: [ helperField ],
    })
})

view.dispatch({
    effects: HelperEffect.of(myHelperObject)
})

However, trying to fetch the value in my MatchDecorataor only returns the initial value of null:

let myDecorator = new MatchDecorator({
    regexp: new RegExp('my_regexp', 'gi'),
    decoration: (match, view) => {
        let myHelperObject = view.state.field(helperField) // Returns null
    }
})

I think I solved it. I just use StateField.init() to set the object. So I don’t need to dispatch an effect, after all.

let view = new EditorView({
    state: EditorState.create({
        […]
        extensions: [ helperField.init(() => myHelperObject) ],
    })
})

Any objections to this approach?

The return in your update method only returns from the inner function, and won’t do what you intended it to do.

Haha, oh yeah. That was stupid. After fixing this, it works. However, only after a first modification on the state.doc since the initial value is null.

I never need to update the helper object, so probably I’ll stay with the initial value and get rid of the whole state effect.

Thanks for your support, marjin!