Facets as Composable Extension Points

An extensible system, at its base, is a system that allows people toadd additional functionality that was not anticipated by the coresystem.

A good extensible system also makes sure multiple extensions thatdon't know anything about each other can be combined, and compose inways that don't cause problems.

The problem has several aspects.

Composition: Multiple extensions attaching to a given extensionpoint must have their effects combined in a predictable way.

Precedence: In cases where combining effects isorder-sensitive, it must be easy to reason about and control theorder of the extensions.

Grouping: Many extensions will need to attach to a number ofextension points, or even pull in other extensions that they dependon.

Change: The effect produced by extensions may depend on otheraspects of the system state, or be explicitly reconfigured.

This post tries to explain CodeMirror's(a code editor library) approach to solving this problem.

Facets and the Editor State

A facet, in this system, defines an extension point. It takes anynumber of input values and produces an output value. Examples offacets are...

Event handlers, where individual extension can define function thathandle a given event.

Editor configuration, like the tab size and whether content isread-only.

The set of markers to style the content with (for example syntaxhighlighting).

The set of gutters to show next to the content.

When defining an editor state, you pass in a collection of facet inputvalues, which together define the behavior of the editor. In a givenstate, each facet has zero or more inputs. Their output value somehowcombines these���it may simply be an array of input values, or someother function of them.

Facets are defined as values and (optionally) exported so thatthird-party code can provide inputs. The core system defines a numberof facets, but facets defined outside it work exactly the same asthose defined by the core.

Precedence

Often input values need a well-defined order. For event handlers, thisdetermines which handlers get to go first, for example. For gutters,it defines the order in which they are displayed, and so on.

The order in which the facet values are provided when configuring theeditor state provides a predictable ordering for the facet inputs, andis used as a basis for precedences. So if you provide two handlers fora given event, the one that you provide first will take precedence.

But sometimes the code that defines a facet value knows that it shouldhave a given precedence, and you don't want to be dependent on theprogrammer using this extension to get the relative order right. Forcases like this, the system also supports explicit precedence tagging,which assigns one of five (���highest��� to ���lowest���) precedencecategories to a given extension. The actual precedence of inputs isdetermined first by category, then by order.

Grouping

A given extension often needs to provide multiple facet values. Forexample, a code folding system needs to define a state field to holdinformation on what is currently folded, key bindings to controlfolding, a gutter to display fold markers, and a CSS module to styleits UI elements.

To make this easy, extensions can be provided as arbitrarily deeplynested arrays. A function exported from an extension module can returnan array of extensions, which can be included in a biggerconfiguration by just putting the result of calling that functionalongside other extensions in the array used to define the editorstate.

The actual ordering of the extensions is created by recursivelyflattening this array, resulting in a single array of input values,each tagged with a facet. These are then reordered based on explicitlyassigned precedence categories and split by facet to provide theactual inputs for a given facet.

Deduplication

Because different extensions may depend on each other, and thusinclude each other's extension trees in their own extension tree, itbecomes likely that people will end up with duplicated extensions intheir configuration. For example, both the line numbers extensions andthe fold gutter extension might use an extension that defines editorgutter infrastructure.

Because it can be wasteful or even break things to actually includesuch shared dependencies multiple times, CodeMirror's extension systemdeduplicates extensions by identity���if the same extension value occursmultiple times in a configuration, only the one in thehighest-precedence position is used.

As long as extensions that run the risk of accidentally being usedmultiple times take care to statically define their extension objects,and always return the same object, this makes sure such shareddependencies don't cause problems. Things like extensionconfiguration, which might be different across uses of the extension,can often be put in a separate facet, which combines the parametersgiven by multiple users in some reasonable way, or raises an error ifthey conflict.

Reconfiguration

Some of the inputs to facets might change over the lifetime of aneditor. And just creating a fully new editor state with a newconfiguration may lose information (say, the undo history) containedin that state.

Thus, existing states can be reconfigured. The system supports twokinds of reconfiguration: full reconfiguration, where the root of theextension tree is replaced with a completely new set of extensions, orcompartment reconfiguration, where you tag part of your initialextension tree as a compartment, and then later replace only that partof the tree.

In either case, the data-driven approach to configuration (the codecan compare the old and the new inputs) allows the system to preserveparts of the state that didn't change, and update the values of facetswhose inputs did change.

Dynamic Inputs

Systems with a complicated user interface tend to, at some point, growsome form of incremental computation support. They need to keep thethings they show to the user consistent with their state, but theirstate is large and complicated, and can change in all kinds of ways.

A code editor is definitely a complicated user interface, and becauseit must be as responsive as possible, has a strong need to avoidneedless recomputations. Facets help with this. For a start, theyavoid recomputing output values when the facet's inputs stay the same,so code that depends on the facet can do a quick identity-equalitytest on the facet's current output value to determine whether itchanged.

But it is also possible to define dynamic inputs for facets, whichprovide an input value (or a set of values) that is computed fromother facets or other aspects of the editor state. The state updatesystem makes sure that, if any of the dependencies change, the inputvalue is recomputed���and, if it is different than its old value, thefacet value is also recomputed, as are any dynamic values thatdepended on that facet, and so on.

Representation

Because most facets, for a given configuration, have a static value,their representation can be optimized in a way that avoids doing anywork on state updates. This is helpful, because the editor state tendsto be updated multiple times per second, and we don't want to do anysuperfluous work during those updates.

When a given configuration is resolved, facets are categorized aseither static or dynamic, depending on whether they have dynamicinputs. Each facet is assigned an address in either the static valuesarray (which is reused as-is on any state update that doesn't changethe configuration) or the dynamic values array. The latter is copiedon state updates, and the dependency graph between facets (and otherstate fields) is used to determine which of the values need to berecomputed and which can be kept as they are.

Facets with no inputs at all aren't even stored in the state. Whenqueried, the value that facet has with zero inputs can be looked upfrom the facet.

 •  0 comments  •  flag
Share on Twitter
Published on June 06, 2022 16:00
No comments have been added yet.


Marijn Haverbeke's Blog

Marijn Haverbeke
Marijn Haverbeke isn't a Goodreads Author (yet), but they do have a blog, so here are some recent posts imported from their feed.
Follow Marijn Haverbeke's blog with rss.