Code example for applying decorations asynchronously

I’m trying to figure out the proper way to apply decorations asynchronously. In my case, I have a spell-checking library that only works async. There are a couple of threads that already talk about this (I’ve linked them below), but they don’t have many details on how to do it or any code examples. I’ve spent a few days attempting to solve it, but I’ve gotten nowhere.

Is there a simple example of how to apply decorations async? This is a common need for linting, spell checking, grammar checking, etc. So an example of the proper way to do it would be immensely helpful. Maybe it could be added to the example section of the docs?

Related Threads:

1 Like

Not really, but there’s not a lot to it. Start your request as appropriate (probably a debounce on document updates) from a view plugin. When it returns, dispatch a transaction with metadata that tells your state field to add the decorations. Either drop responses that are outdated, or record mappings in order to be able to map them to the updated document.

There is a github gist in the Asynchronous Initialization one, but here’s an updated version for syntax highlighting: ProseMirror CodeBlock Syntax Highlighting using CM6 · GitHub. It’s asynchronous because we’re dynamically importing languages.

1 Like

Thanks for the replies! I’ve implemented what you’ve suggested, but it creates an infinite loop the minute the doc changes. I think the dispatch function calls the view update, which then calls dispatch again. What am I missing?

function update(view) {
    const { doc } = view.state.tr
    // Find all nodes with text to send to the spell checker
    const matching = findBlockNodes(doc).filter(item => item.node.isTextblock && !item.node.type.spec.code)

    // Simulate sending the text to a spell checker
    setTimeout(() => {
        console.log('UPDATE');

        let newDecs = decorateNodes(matching)
        view.dispatch(view.state.tr.setMeta("asyncDecorations", newDecs))
    }, 100)
}

let debouncedUpdate = debounce(update, 1000)

let plugin = new Plugin({
    props: {
        decorations(state) { return this.getState(state) },
    },
    state: {
        init: (config, state) => {
            return DecorationSet.create(state.doc, [])
        },
        apply: (tr, decorationSet) => {
            const asyncDecs = tr.getMeta("asyncDecorations")

            if (asyncDecs === undefined && !tr.docChanged) {
                return decorationSet
            }

            console.log('received async decorations', asyncDecs);

            return DecorationSet.create(tr.doc, asyncDecs)
        },
    },
    view: function(view) {
        return {
            update(view, prevState) {
                debouncedUpdate(view)
            }
        }
    }
})

I think the dispatch function calls the view update, which then calls dispatch again. What am I missing?

Yeah, that seems to be the issue. Instead of calling the api from within update, you want to initialize the debounced API request from within the apply method, and just use the update method of the view plugin to keep the plugin up to do with the editor.

E.g.


let EditorView;

const apiRequest = (matching) => {
  setTimeout(() => {
    let newDecs = decorateNodes(matching)
    EditorView.dispatch(EditorView.state.tr.setMeta("asyncDecorations", newDecs))
  }, 100);
}

let debouncedApiRequest = debounce(apiRequest , 1000);

let plugin = new Plugin({
    props: {
        decorations(state) { return this.getState(state) },
    },
    state: {
        init: (config, state) => {
            return DecorationSet.create(state.doc, [])
        },
        apply: (tr, decorationSet) => {
            if (tr.docChanged) {
              const { doc } = view.state.tr
              // Find all nodes with text to send to the spell checker
              const matching = findBlockNodes(doc).filter(item => item.node.isTextblock && !item.node.type.spec.code);
              debouncedApiRequest(matching);
            }

            const asyncDecs = tr.getMeta("asyncDecorations")
            if (asyncDecs === undefined && !tr.docChanged) {
                return decorationSet
            }

            console.log('received async decorations', asyncDecs);

            return DecorationSet.create(tr.doc, asyncDecs)
        },
    },
    view: function(view) {
        return {
            update(view, prevState) {
                EditorView = view;
            }
        }
    }
})
4 Likes

Thanks! That helped me get going in the right direction.