Implementing a firstLineNumber option in CM6

Hi! So I’m looking at doing a CodeMirror 5 to 6 upgrade and one of the options we use is firstLineNumber. I figured that would be a good place to start learning some of the new API and how it all works together.

I was able to come up with two different implementations that both work, but I’m not sure I’m doing it in the spirit of how it was intended.

The first is a very naive implementation that creates a new config object for the lineNumbers extension then returns the extension to be given to the editor and a function that can be called to updated it:

function makeLineNumbers(initialValue)
{
    const makeConfig = (startLine) => ({
        formatNumber: (lineNumber, state) =>  "" + (lineNumber + startLine - 1);
    });

    const startLine = new Compartment();
    const startLineFacet = Facet.define({
        combine: values => values.length ? values[0] : 1
    });

    const configuredLineNumbers = startLine.of([
        startLineFacet.of(initialValue),
        lineNumbers(makeConfig(initialValue))
    ]);

    function update(newStart, editorState)
    {
        const currentValue = editorState.facet(startLineFacet);

        if (currentValue === newStart)
            return [];

        return [
            startLine.reconfigure([
                startLineFacet.of(newStart),
                lineNumbers(makeConfig(newStart))
            ])
        ];
    }

    return [configuredLineNumbers, update];
}

(small note: I’m returning arrays in the update function for implementation detail convenience reasons)

The second implementation was an effort to better encapsulate the current state. It doesn’t create new configs for the lineNumbers extension but rather updates a facet value and reconfigures the compartment :

function makeLineNumbers(initial)
{
    const firstLineNumber = Facet.define({
        combine: values => values.length ? values[0] : 1
    });

    const config = {
        formatNumber: (lineNumber, state) => {
            const start = state.facet(firstLineNumber);
            return "" + (lineNumber + start - 1);
        }
    };

    const dynamicLineNumbers = new Compartment();

    function updateIfNeeded(newStart, editorState)
    {
        const currentValue = editorState.facet(firstLineNumber);

        if (currentValue === newStart)
            return [];

        return [
            dynamicLineNumbers.reconfigure([
                firstLineNumber.of(newStart),
                lineNumbers(config)
            ])
        ];
    }

    const initialLineNumbers = dynamicLineNumbers.of([
        firstLineNumber.of(initial),
        lineNumbers(config)
    ]);

    return [initialLineNumbers, updateIfNeeded];
}

I thought there might be some way to say the lineNumbers extension depends on the startLineNumber facet directly, then encapsulate that in a way where I only needed to call startLineNumber.of(newValue) when reconfiguring the compartment, but it seems like I always need to pass a new lineNumbers(config) during the reconfiguration (even though the config object itself didn’t change".

So I’m curious what the “right way” to do this is. Am I overthinking/over-engineering it in example 2?

Thanks!
- Randy

Why not just directly use the configured lineNumbers extension (possibly inside of a compartment if this needs to change in an existing state)?

Just so I’m clear what you mean by directly using the extension, you mean something like this?

function makeLineNumbers(initialLineNumber)
{
    const makeConfig = firstLine => ({
        formatNumber: (lineNumber) => "" + (lineNumber + firstLine - 1)
    });

    const firstLine = new Compartment();

    const configuredLineNumbers = firstLine.of([
        lineNumbers(makeConfig(initialLineNumber))
    ]);

    function update(newFirstLineNumber)
    {
        return [
            firstLine.reconfigure([
                lineNumbers(makeConfig(newFirstLineNumber))
            ])
        ];
    }

    return [configuredLineNumbers, update];
}

There’s not really a reason I chose to not do it that way, it just felt like creating a new configuration object each time wasn’t what the API intended. I guess my original intent was to try to create an extension that relied solely on being able to reconfigure a single number (though I was mostly unsuccessful).

That approach seems fine — I wouldn’t even try to include the compartment in the abstraction. Just let user code do that.

1 Like