or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

changes.mdcharacter-utils.mdeditor-state.mdextensions.mdindex.mdrange-sets.mdselection.mdtext.mdtransactions.md
tile.json

extensions.mddocs/

Extension System

Flexible extension architecture using facets and state fields for configurable editor behavior and plugin development.

Capabilities

Facet System

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

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)); // 1

Compartments

Compartments 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 extension

Precedence System

The 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
  ]
});

Advanced Facet Usage

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());

Extension Composition

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
});

Types

/**
 * 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;
}