Editor state data structures for the CodeMirror code editor
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Flexible extension architecture using facets and state fields for configurable editor behavior and plugin development.
Facets provide a way to make aspects of the editor configurable by allowing extensions to contribute values that are combined into a single output.
/**
* A facet is a labeled value that is associated with an editor state.
* It takes inputs from any number of extensions, and combines those into a single output value.
*/
class Facet<Input, Output = readonly Input[]> {
/** Define a new facet */
static define<Input, Output = readonly Input[]>(config?: FacetConfig<Input, Output>): Facet<Input, Output>;
/** Returns a facet reader for this facet */
get reader(): FacetReader<Output>;
/** Returns an extension that adds the given value to this facet */
of(value: Input): Extension;
/** Create an extension that computes a value for the facet from a state */
compute(deps: readonly Slot<any>[], get: (state: EditorState) => Input): Extension;
/** Create an extension that computes zero or more values for this facet from a state */
computeN(deps: readonly Slot<any>[], get: (state: EditorState) => readonly Input[]): Extension;
/** Create facet source with a state field as input */
from<T extends Input>(field: StateField<T>): Extension;
from<T>(field: StateField<T>, get: (value: T) => Input): Extension;
/** @internal - The default value when no inputs are present */
readonly default: Output;
}
interface FacetConfig<Input, Output> {
/** How to combine the input values into a single output value */
combine?: (value: readonly Input[]) => Output;
/** How to compare output values to determine whether the facet changed */
compare?: (a: Output, b: Output) => boolean;
/** How to compare input values to avoid recomputing when inputs haven't changed */
compareInput?: (a: Input, b: Input) => boolean;
/** Forbids dynamic inputs to this facet */
static?: boolean;
/** Extensions to be added when this facet is provided */
enables?: Extension | ((self: Facet<Input, Output>) => Extension);
}Usage Examples:
import { Facet, StateField, EditorState } from "@codemirror/state";
// Define a simple facet for theme colors
const themeColor = Facet.define<string, string>({
combine: values => values.length ? values[0] : "default"
});
// Define a facet that collects multiple values
const plugins = Facet.define<{name: string, handler: Function}>();
// Use facets in extensions
const darkTheme = themeColor.of("dark");
const lightTheme = themeColor.of("light");
const pluginA = plugins.of({name: "plugin-a", handler: () => {}});
const pluginB = plugins.of({name: "plugin-b", handler: () => {}});
// Create state with facet values
const state = EditorState.create({
extensions: [darkTheme, pluginA, pluginB]
});
// Read facet values
console.log(state.facet(themeColor)); // "dark"
console.log(state.facet(plugins)); // [{name: "plugin-a", ...}, {name: "plugin-b", ...}]State fields store additional information in an editor state and keep it in sync with the rest of the state.
/**
* Fields can store additional information in an editor state,
* and keep it in sync with the rest of the state.
*/
class StateField<Value> {
/** Define a state field */
static define<Value>(config: StateFieldSpec<Value>): StateField<Value>;
/** Returns an extension that enables this field and overrides initialization */
init(create: (state: EditorState) => Value): Extension;
/** State field instances can be used as Extension values */
get extension(): Extension;
}
interface StateFieldSpec<Value> {
/** Creates the initial value for the field when a state is created */
create: (state: EditorState) => Value;
/** Compute a new value from the field's previous value and a transaction */
update: (value: Value, transaction: Transaction) => Value;
/** Compare two values of the field, returning true when they are the same */
compare?: (a: Value, b: Value) => boolean;
/** Provide extensions based on this field */
provide?: (field: StateField<Value>) => Extension;
/** Function used to serialize this field's content to JSON */
toJSON?: (value: Value, state: EditorState) => any;
/** Function that deserializes the JSON representation */
fromJSON?: (json: any, state: EditorState) => Value;
}Usage Examples:
// Define a counter field that tracks document changes
const changeCount = StateField.define<number>({
create: () => 0,
update: (count, tr) => tr.docChanged ? count + 1 : count
});
// Define a field that tracks selection history
const selectionHistory = StateField.define<EditorSelection[]>({
create: (state) => [state.selection],
update: (history, tr) => {
if (tr.selection && !tr.selection.eq(tr.startState.selection)) {
return [...history.slice(-10), tr.selection]; // Keep last 10 selections
}
return history;
},
compare: (a, b) => a.length === b.length && a.every((sel, i) => sel.eq(b[i]))
});
// Use fields in state
const state = EditorState.create({
doc: "Hello world",
extensions: [changeCount, selectionHistory]
});
// Read field values
console.log(state.field(changeCount)); // 0
console.log(state.field(selectionHistory).length); // 1
// Update state and see field changes
const transaction = state.update({changes: {from: 0, insert: "Hi "}});
const newState = transaction.state;
console.log(newState.field(changeCount)); // 1Compartments allow dynamic reconfiguration of parts of the editor state.
/**
* Extension compartments can be used to make a configuration dynamic.
* By wrapping part of your configuration in a compartment, you can later
* replace that part through a transaction.
*/
class Compartment {
/** Create an instance of this compartment to add to your state configuration */
of(ext: Extension): Extension;
/** Create an effect that reconfigures this compartment */
reconfigure(content: Extension): StateEffect<unknown>;
/** Get the current content of the compartment in the state, or undefined if not present */
get(state: EditorState): Extension | undefined;
/** @internal - Static effect type for reconfiguration */
static reconfigure: StateEffectType<{compartment: Compartment, extension: Extension}>;
}Usage Examples:
// Create a compartment for theme configuration
const themeCompartment = new Compartment();
// Initial state with light theme
const state = EditorState.create({
extensions: [
themeCompartment.of(lightTheme),
// other extensions...
]
});
// Reconfigure to dark theme
const switchToDark = state.update({
effects: themeCompartment.reconfigure(darkTheme)
});
// Check current compartment content
console.log(themeCompartment.get(state)); // Returns light theme extension
console.log(themeCompartment.get(switchToDark.state)); // Returns dark theme extensionThe precedence system controls the order in which extensions are applied.
/**
* By default extensions are registered in the order they are found.
* Individual extension values can be assigned a precedence to override this.
*/
const Prec = {
/** The highest precedence level, for extensions that should end up near the start */
highest: (ext: Extension) => Extension,
/** A higher-than-default precedence */
high: (ext: Extension) => Extension,
/** The default precedence */
default: (ext: Extension) => Extension,
/** A lower-than-default precedence */
low: (ext: Extension) => Extension,
/** The lowest precedence level */
lowest: (ext: Extension) => Extension
};Usage Examples:
// Apply precedence to extensions
const highPriorityTheme = Prec.highest(darkTheme);
const lowPriorityPlugin = Prec.low(somePlugin);
const state = EditorState.create({
extensions: [
normalExtension,
highPriorityTheme, // Will be processed first
anotherExtension,
lowPriorityPlugin // Will be processed last
]
});Examples of sophisticated facet patterns for complex configuration scenarios.
// Computed facet that depends on other facets
const computedTheme = Facet.define<{bg: string, fg: string}>({
combine: values => values.length ? values[0] : {bg: "white", fg: "black"}
});
const themeProvider = computedTheme.compute([EditorState.tabSize, someOtherFacet], state => {
const tabSize = state.facet(EditorState.tabSize);
const other = state.facet(someOtherFacet);
return {
bg: tabSize > 2 ? "light-blue" : "white",
fg: other.darkMode ? "white" : "black"
};
});
// Multi-value facet that provides multiple inputs
const eventHandlers = Facet.define<{event: string, handler: Function}>();
const multiEventProvider = eventHandlers.computeN([someStateField], state => {
const fieldValue = state.field(someStateField);
return [
{event: "click", handler: () => console.log("click", fieldValue)},
{event: "keydown", handler: () => console.log("keydown", fieldValue)}
];
});
// Facet derived from state field
const derivedFacet = Facet.define<string>();
const fieldBasedProvider = derivedFacet.from(someStringField, value => value.toUpperCase());Patterns for combining and organizing extensions.
/**
* Extensions can be nested in arrays arbitrarily deep
*/
type Extension = {extension: Extension} | readonly Extension[];
// Helper for creating extension bundles
function createThemeExtension(colors: {primary: string, secondary: string}): Extension {
return [
themeColor.of(colors.primary),
secondaryColor.of(colors.secondary),
computedTheme.compute([], () => ({
bg: colors.primary,
fg: colors.secondary
}))
];
}
// Extension with conditional parts
function createConditionalExtension(options: {enableFeatureA: boolean, enableFeatureB: boolean}): Extension {
const extensions: Extension[] = [
coreExtension,
options.enableFeatureA ? featureAExtension : [],
options.enableFeatureB ? featureBExtension : []
];
return extensions;
}
// Extension that provides other extensions
const masterExtension = Facet.define<Extension>({
enables: self => self.reader // Self-referential to provide collected extensions
});/**
* A facet reader can be used to fetch the value of a facet, but not to define new values
*/
type FacetReader<Output> = {
id: number;
default: Output;
tag: Output;
};
/**
* Slot type for facet dependencies
*/
type Slot<T> = FacetReader<T> | StateField<T> | "doc" | "selection";
/**
* Extension type - the base type for all extensions
*/
type Extension = {extension: Extension} | readonly Extension[];
/**
* Configuration type for dynamic slots
*/
interface DynamicSlot {
create(state: EditorState): SlotStatus;
update(state: EditorState, tr: Transaction): SlotStatus;
reconfigure(state: EditorState, oldState: EditorState): SlotStatus;
}