Hiding (replacing) lines that begin with a certain signifier

Hey, I’m totally new to Codemirror so I’m sure I’m doing something very wrong here :sweat_smile:

I’m trying to create a plugin that will automatically/always replace lines that begin with a certain signifier prefix with a tag/badge containing the text replacement. Here’s the attempt:

function hideLines(view, prefix, replacement) {
    let decs = [];
    let startLine = view.state.doc.lineAt(0);
    let endLine = view.state.doc.lineAt(view.state.doc.length);
    for (let i = startLine.number; i <= endLine.number; i++) {
        let line = view.state.doc.line(i);
        if (line.text.startsWith(prefix)) {
            let d = Decoration.replace({ widget: new BadgeWidget(replacement) });
            decs.push(d.range(line.from, line.to));
        }
    }
    return Decoration.set(decs);
}

const hideLinesPlugin = (prefix, replacement) => ViewPlugin.fromClass(class {
    constructor(view) {
        this.decorations = hideLines(view, prefix, replacement);
    }

    update(update) {
        if (update.docChanged) {
            this.decorations = hideLines(update.view, prefix, replacement);
        }
    }
}, {
    decorations: v => v.decorations,
});

which is mostly cobbled together from the examples in the docs (BadgeWidget works correctly and is irrelevant to this problem). After passing a created plugin to extensions, everything seems to work properly, but when I change the whole document (after, say, loading a file over the network) with

view.dispatch({
    changes: { from: 0, to: view.state.doc.length, insert: newDocument },
});

I get some weird behavior of the document being partially displayed (not all text is there), along with a warning Measure loop restarted more than 5 times in the console.

Note that these are long lines I’m replacing, and lineWrapping is on, so replacing one of these lines does dramatically alter the viewport size; as such, I expect the way I’m doing this is causing some kind of thrashing when calculating the layout.

Any help is greatly appreciated!

I figure this approach is fundamentally broken, per this note in the docs:

A facet that determines which decorations are shown in the view. Decorations can be provided in two ways—directly, or via a function that takes an editor view.
Only decoration sets provided directly are allowed to influence the editor’s vertical layout structure. The ones provided as functions are called after the new viewport has been computed, and thus must not introduce block widgets or replacing decorations that cover line breaks.

I’ve managed to get a working solution for now with the following approach:

const addHiddenBadge = StateEffect.define();

const hiddenField = StateField.define({
    create() {
        return Decoration.none;
    },
    update(hiddens, tr) {
        hiddens = hiddens.map(tr.changes);
        for (let e of tr.effects) {
            if (e.is(addHiddenBadge)) {
                let d = e.value.decoration;
                hiddens = hiddens.update({
                    add: [ e.value.decoration.range(e.value.from, e.value.to) ],
                });
            }
        }
        return hiddens;
    },
    provide: f => EditorView.decorations.from(f),
});

function prefixLineHider(prefix, replacement) {
    const decoration = Decoration.replace({ widget: new BadgeWidget(replacement) });
    return view => {
        let effects = [];
        let pos = 0;
        while (pos <= view.state.doc.length) {
            let line = view.state.doc.lineAt(pos);
            if (line.text.startsWith(prefix)) {
                effects.push(addHiddenBadge.of({ from: line.from, to: line.to, decoration }));
            }
            pos = line.to + 1;
        }
        view.dispatch({ effects });
    }
}

Where hideLines = prefixLineHider(...) gives a function that can hide the lines imperatively any time. This works in my application since I am programmatically adding the lines I want to hide, but I’m still curious if there’s a better way to accomplish this as a plugin or something of the sort.

It should be possible to use similar update logic to your initial view plugin in a state field, avoiding the need for the effect.

Ah, thanks! I guess I didn’t realize that was “okay”. Here is the final solution which works perfectly just passed as an extension:

function hideLinesByPrefixField(prefix, replacement) {
    const rep = Decoration.replace({ widget: new BadgeWidget(replacement) });
    return StateField.define({
        create() {
            return Decoration.none;
        },
        update(hiddens, tr) {
            hiddens = hiddens.map(tr.changes);
            let pos = 0;
            let newHiddens = [];
            while (pos < tr.newDoc.length) {
                let line = tr.newDoc.lineAt(pos);
                if (line.text.startsWith(prefix)) {
                    newHiddens.push(rep.range(line.from, line.to));
                }
                pos = line.to + 1;
            }
            return hiddens.update({ add: newHiddens });
        },
        provide: f => EditorView.decorations.from(f),
    });
}