Skip to content

Extending

Kodemirror's extension system lets you add behavior, state, and visual elements to the editor. Extensions compose freely — you can combine any number of them by nesting them in lists, and the system flattens and deduplicates them during configuration.

Extension basics

The Extension interface is implemented by facets, state fields, view plugins, compartments, and wrapper types. You pass extensions to EditorStateConfig when creating state:

val session = rememberEditorSession(
    doc = "hello",
    extensions = EditorState.tabSize.of(2) +
        myStateField +
        myViewPlugin.asExtension()
)

Extensions compose using the + operator. They can be nested arbitrarily — the system flattens them.

Facets

A Facet<Input, Output> collects values from multiple extension providers and combines them into a single output. It is the primary way to expose configuration.

Defining a facet

// Collect all values into a list
val myFacet = Facet.define<String, List<String>>(
    combine = { values -> values }
)

// Take the first value
val tabSize = Facet.define<Int, Int>(
    combine = { values -> values.firstOrNull() ?: 4 }
)

// Boolean "any" combiner
val allowMulti = Facet.define<Boolean, Boolean>(
    combine = { values -> values.any { it } },
    static = true
)

The static flag means the facet cannot depend on dynamically computed state.

Providing values

// Static value
myFacet.of("hello")

// Computed from state (recomputed when dependencies change)
myFacet.compute(listOf(Slot.Doc)) { state ->
    "Document has ${state.doc.lines} lines"
}

// Multiple values from one provider
myFacet.computeN(listOf(Slot.Doc)) { state ->
    listOf("line count: ${state.doc.lines}", "length: ${state.doc.length}")
}

// Derived from a state field
myFacet.from(myField) { fieldValue -> fieldValue.toString() }

Dependencies are expressed as Slot values:

Slot Triggers recomputation when...
Slot.Doc the document changes
Slot.Selection the selection changes
Slot.FacetSlot(reader) a facet value changes
Slot.FieldSlot(field) a state field changes

Reading facets

val value = state.facet(myFacet)

FacetReader

FacetReader is the read-side interface of a Facet. Use FacetReader when code only needs to read from a facet, not provide values to it. This makes APIs more flexible — callers can pass either a Facet or a computed facet reader.

// A function that reads a facet without needing to know its input type
fun readAnyFacet(state: EditorState, reader: FacetReader<Int>) {
    val value: Int = state.facet(reader)
}

Facet<I, O> implements FacetReader<O>, so you can pass a facet anywhere a reader is expected.

State fields

A StateField<Value> stores a value that persists across transactions. On every transaction, its update function produces the next value.

Defining a state field

val editCount = StateField.define(StateFieldSpec(
    create = { _ -> 0 },
    update = { value, tr ->
        if (tr.docChanged) value + 1 else value
    }
))

The field itself is an Extension, so you include it in your extensions list.

Reading a state field

val count = state.field(editCount)

Providing to a facet

A state field can automatically feed its value into a facet:

val decorationField = StateField.define(StateFieldSpec(
    create = { state -> computeDecorations(state) },
    update = { decos, tr ->
        if (tr.docChanged) computeDecorations(tr.state) else decos
    },
    provide = { field ->
        decorations.from(field)
    }
))

State effects

StateEffect<Value> values carry typed data through transactions without modifying the document. They are the mechanism for inter-extension communication.

Defining and dispatching

val togglePanel = StateEffect.define<Boolean>()

// Dispatch
view.dispatch(TransactionSpec(
    effects = listOf(togglePanel.of(true))
))

Reading effects in a state field

val panelOpen = StateField.define(StateFieldSpec(
    create = { false },
    update = { value, tr ->
        var result = value
        for (effect in tr.effects) {
            val toggle = effect.asType(togglePanel)
            if (toggle != null) result = toggle.value
        }
        result
    }
))

Position-dependent effects

If your effect contains document positions, provide a map function so positions update when the document changes:

val addBookmark = StateEffect.define<Int>(
    map = { pos, changes -> changes.mapPos(pos) }
)

Built-in effects

  • StateEffect.reconfigure — replace a compartment's extensions
  • StateEffect.appendConfig — add extensions to the configuration

View plugins

A ViewPlugin<V> creates a PluginValue instance that receives ViewUpdate notifications on every state change. View plugins are used for behavior that needs to react to the view — computing decorations, responding to focus changes, etc.

Defining a view plugin

class MyPlugin(private val view: EditorSession) : PluginValue {
    override fun update(update: ViewUpdate) {
        if (update.docChanged) {
            // react to document changes
        }
    }

    override fun destroy() {
        // cleanup
    }
}

val myPlugin = ViewPlugin.define { view -> MyPlugin(view) }

Include it in extensions with myPlugin.asExtension().

Configuring a plugin with PluginSpec

ViewPlugin.define accepts an optional configure lambda that customizes the PluginSpec. This is how you attach decoration providers, event handlers, or other plugin-level options:

val myPlugin = ViewPlugin.define(
    create = { view -> MyPlugin(view) },
    configure = {
        // `this` is a PluginSpec — copy it with new properties
        copy(
            decorations = { plugin -> (plugin as MyPlugin).decorations },
            eventHandlers = mapOf("keydown" to { plugin, event -> false })
        )
    }
)

Plugins with decorations

To contribute decorations, implement DecorationSource:

class HighlightPlugin(state: EditorState) : DecorationSource {
    override var decorations: DecorationSet = buildDecos(state)
        private set

    override fun update(update: ViewUpdate) {
        if (update.docChanged) {
            decorations = buildDecos(update.state)
        }
    }

    private fun buildDecos(state: EditorState): DecorationSet {
        val builder = RangeSetBuilder<Decoration>()
        // add decorations...
        return builder.finish()
    }
}

val highlightPlugin = ViewPlugin.define(
    create = { view -> HighlightPlugin(view.state) },
    configure = {
        copy(decorations = { plugin ->
            (plugin as? HighlightPlugin)?.decorations ?: RangeSet.empty()
        })
    }
)

Accessing a plugin instance

val pluginValue = view.plugin(myPlugin)

Decorations

Decorations add visual annotations to the editor without modifying the document. There are four types:

Mark decorations

Apply inline styling to a range of text:

Decoration.mark(MarkDecorationSpec(
    style = SpanStyle(
        color = Color.Red,
        fontWeight = FontWeight.Bold
    )
))

In upstream CodeMirror, marks add CSS classes. In Kodemirror, marks apply Compose SpanStyle values through AnnotatedString.

Widget decorations

Insert a composable at a position:

class InfoWidget(private val message: String) : WidgetType() {
    @Composable
    override fun Content() {
        Text(message, style = TextStyle(color = Color.Gray, fontSize = 10.sp))
    }
}

Decoration.widget(WidgetDecorationSpec(
    widget = InfoWidget("note"),
    side = 1  // after the position
))

Upstream CodeMirror widgets implement toDOM() returning an HTMLElement. Kodemirror widgets implement @Composable Content().

Line decorations

Style an entire line:

Decoration.line(LineDecorationSpec(
    style = SpanStyle(background = Color(0x20FF0000))
))

Replace decorations

Hide or replace a range, optionally with a widget:

Decoration.replace(ReplaceDecorationSpec(
    widget = FoldWidget()  // optional replacement widget
))

Building decoration sets

Decorations are collected into a DecorationSet (alias for RangeSet<Decoration>) using a builder:

val builder = RangeSetBuilder<Decoration>()
builder.add(from = 5, to = 10, value = myMarkDecoration)
builder.add(from = 15, to = 15, value = myWidgetDecoration)
val decoSet: DecorationSet = builder.finish()

Positions must be added in order (ascending from).

Compartments

A Compartment wraps a subset of extensions that can be dynamically replaced at runtime without recreating the entire state.

val themeCompartment = Compartment()

// Initial configuration
val state = EditorState.create(EditorStateConfig(
    extensions = themeCompartment.of(oneDark)
))

// Later, switch theme
val tr = state.update(TransactionSpec(
    effects = listOf(
        themeCompartment.reconfigure(lightEditorTheme.let { editorTheme.of(it) })
    )
))

This is the recommended way to handle configuration that changes at runtime — language switching, theme toggling, enabling/disabling features.

Precedence

By default, extensions are processed in the order they appear. You can override this with Prec:

Prec.highest(myExtension)   // runs first
Prec.high(myExtension)
Prec.default(myExtension)   // default level
Prec.low(myExtension)
Prec.lowest(myExtension)    // runs last

This affects how facet values are ordered when combined. For example, keymaps at higher precedence get first chance to handle a key event.

Putting it together

Here is how the search extension composes these primitives:

fun search(): Extension {
    return searchQueryField +
        searchPanelOpenField +
        // Computed facet — show/hide the search panel
        showPanels.compute(
            listOf(Slot.FieldSlot(searchPanelOpenField))
        ) { state ->
            if (state.field(searchPanelOpenField))
                listOf(Panel(top = true, content = @Composable { SearchPanel() }))
            else emptyList()
        } +
        // View plugin with decorations for match highlighting
        ViewPlugin.define(
            create = { view -> SearchHighlightPlugin(view.state) },
            configure = {
                copy(decorations = { plugin ->
                    (plugin as? SearchHighlightPlugin)?.decorations
                        ?: RangeSet.empty()
                })
            }
        ).asExtension() +
        // Keybindings
        keymap.of(searchKeymap)
}

This single search() call provides state management, a UI panel, decorations, and key bindings — all composed from the same extension primitives available to any user code.