Enable computed facet in facet

I have a use case in which I have some decorations with specs (in this case tooltip content). I want to be able to provide a DecorationSet facet to collect all my “tooltip decorations” and then use FacetConfig.enables to provide that to EditorView.decorations and my custom tooltip facet. Unfortunately, since FacetConfig.enables doesn’t pass an instance to the surrounding facet (in contrast to StateFieldSpec.provide), there isn’t a way to have a facet enable its own dependents.

const decorationTooltips = Facet.define<DecorationSet>({
    enables: [
        // doesn't work because decorationTooltips isn't defined yet!
        // EditorView.decorations.computeN([decorationTooltips], state => state.facet(decorationTooltips))
        // ...also provide to tooltip facet
    ]
});

Right now, I’m introducing a dummy StateField to get the indirection I need, but it feels like there should be a cleaner way to do this:

const decorationTooltipsState = StateField.define<readonly DecorationSet[]>({
    create: () => [],
    update: (value, tr) => tr.state.facet(decorationTooltips),
    // compare, etc.
    provide: field => [
        EditorView.decorations.computeN([field], state => state.field(field))
        // ...also provide to tooltip facet
    ]
});

const decorationTooltips = Facet.define<DecorationSet>({
    enables: decorationTooltipsState
});

Am I missing something and/or misusing facet?

P.S. Could we make Decoration generic on the spec type as well, and just default it to any? That should be backwards compatible, but would allow people to enforce a particular spec type via the type system.

I don’t think a facet is the right place to collect a bunch of cooperating extensions. Does the regular pattern of exporting a function that returns an array of extensions not work in this case?

Making Decoration generic would require exporting and documenting all the Decoration subclasses, which seems like a lot of interface noise for a minor gain (you’ll still usually be accessing custom properties on the specs which are not part of the actual spec types anyway).

I suppose I could use a function like the following,

const decorationTooltipsFacet = Facet.define<DecorationSet>();

function decorationTooltips(){
    return [
        decorationTooltipsFacet,
        EditorView.decorations.computeN([decorationTooltipsFacet], state => state.facet(decorationTooltips)),
        // ...also provide to tooltip facet
    ];
}

but I was hoping to do this in a way that would make the facet “self-enabling”, meaning that I could just decorationTooltipsFacet.from somewhere and not worry about separately enabling the dependents.

For Decoration, I was just thinking to add <T=any> to Decoration/DecorationSet without any subclasses. That way, existing code would resolve the exactly the same (lack of) types that we have now because of the defaulted parameter, but you could also write code like this Facet.define<DecorationSet<MyCustomSpec>> when you want specific types.

A Facet class instance is not an extension, though, so there doesn’t seem to be a reason to include it in enables (in fact, that would be a type error).

It’s a FacetProvider that I’m trying to add to enables though (specifically one from computeN). If I were adding a FacetProvider for a different Facet it would work just fine, but I can’t create my FacetProvider without a reference to the “parent” Facet, which doesn’t exist yet. It’s a chicken-and-egg problem. I can’t create FacetProviders from the Facet I’m in the middle of creating. (See the first code block in the original post for an example of what I’m trying to do).

StateField solves this problem because it creates itself before resolving the dependent extensions in provide and passes itself to that function, so it’s available to create dependents based on itself.

But that’s still an odd thing to do—you’re saying, “as soon as any value is provided for this facet, add this other value as well”. Wouldn’t it make more sense to set up your combine function to do that kind of extra value injection?

Not exactly. It’s more like “as soon as any value is provided for this facet, add it (modified) to these other two facets as well”. Which normally would be a simple use case for compute/computeN, but there’s no way to create the enables array without a reference to the facet that doesn’t yet exist

Oh, right, now I finally get what you want to do. The patch below might help.

Perfect! That’s exactly what I need, thanks