Skip to content

Getting Started

This tutorial walks through creating a minimal Kodemirror editor from scratch in a Kotlin Multiplatform / Compose Multiplatform project.

Prerequisites

  • JDK 11 or later (JDK 21 recommended)
  • Android Studio or IntelliJ IDEA with Kotlin plugin
  • Gradle 8.x

1. Create a Compose Multiplatform project

Use the Kotlin Multiplatform Wizard or create a new Compose Multiplatform project in your IDE.

2. Add dependencies

In your module's build.gradle.kts, add the Kodemirror dependencies:

dependencies {
    // Core
    implementation("com.monkopedia.kodemirror:state:0.3.5")
    implementation("com.monkopedia.kodemirror:view:0.3.5")

    // Commands (undo, redo, indentation, etc.)
    implementation("com.monkopedia.kodemirror:commands:0.3.5")

    // Language infrastructure (syntax highlighting, bracket matching)
    implementation("com.monkopedia.kodemirror:language:0.3.5")

    // Language support (pick the languages you need)
    implementation("com.monkopedia.kodemirror:lang-javascript:0.3.5")

    // Optional: theme
    implementation("com.monkopedia.kodemirror:theme-one-dark:0.3.5")

    // Optional: all-in-one setup bundle
    // implementation("com.monkopedia.kodemirror:basic-setup:0.3.5")
}

3. Create a minimal editor

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.monkopedia.kodemirror.commands.*
import com.monkopedia.kodemirror.lang.javascript.javascript
import com.monkopedia.kodemirror.language.defaultHighlightStyle
import com.monkopedia.kodemirror.language.syntaxHighlighting
import com.monkopedia.kodemirror.state.plus
import com.monkopedia.kodemirror.view.*

@Composable
fun MyEditor() {
    val session = rememberEditorSession(
        doc = "console.log(\"Hello, Kodemirror!\")",
        extensions = lineNumbers +
            history() +
            javascript().extension +
            syntaxHighlighting(defaultHighlightStyle) +
            keymapOf(defaultKeymap)
    )

    KodeMirror(
        session = session,
        modifier = Modifier.fillMaxSize()
    )
}

This gives you: - A JavaScript-aware editor with syntax highlighting - Line numbers in the gutter - Undo/redo with Ctrl-Z / Ctrl-Y / Ctrl-Shift-Z — history() installs its own key bindings, so there is nothing extra to add to the keymap - Standard cursor movement and selection commands

Note that lineNumbers is a value, not a function, and that language factories such as javascript() return a LanguageSupport — use its .extension property to combine it with other extensions.

Or use basicSetup for a batteries-included experience:

import com.monkopedia.kodemirror.basicsetup.basicSetup
import com.monkopedia.kodemirror.lang.javascript.javascript
import com.monkopedia.kodemirror.state.plus
import com.monkopedia.kodemirror.view.*

@Composable
fun MyEditor() {
    val session = rememberEditorSession(
        doc = "console.log(\"Hello!\")",
        extensions = basicSetup + javascript().extension
    )
    KodeMirror(session = session, modifier = Modifier.fillMaxSize())
}

4. Understanding the pattern

Kodemirror uses rememberEditorSession to manage editor state internally:

  1. SessionrememberEditorSession creates an EditorSession that holds the EditorState (document, selection, extension state) and handles updates.
  2. ViewKodeMirror renders the session as a Compose UI.
  3. Transactions — when the user types or triggers a command, a Transaction is created with the changes and applied automatically.
User action → Transaction → EditorSession updates state → recomposition

Extensions are combined using the + operator:

val extensions = lineNumbers + history() + javascript().extension

5. Adding more features

Bracket matching

import com.monkopedia.kodemirror.language.bracketMatching

// Add to your extensions:
basicSetup + javascript().extension + bracketMatching()

Autocompletion

import com.monkopedia.kodemirror.autocomplete.*

basicSetup + autocompletion(CompletionConfig(
    override = listOf(
        completeFromList(listOf(
            Completion(label = "console", type = "variable"),
            Completion(label = "document", type = "variable"),
            Completion(label = "window", type = "variable")
        ))
    )
))
import com.monkopedia.kodemirror.search.*

basicSetup + search()

Custom theme

import com.monkopedia.kodemirror.view.*

val myTheme = EditorTheme(
    background = Color(0xFF1E1E1E),
    foreground = Color(0xFFD4D4D4),
    cursor = Color.White,
    selection = Color(0xFF264F78),
    dark = true
)

basicSetup + editorTheme.of(myTheme)

Or use the Material Design bridge for automatic theme integration:

import com.monkopedia.kodemirror.materialtheme.rememberMaterialEditorTheme

@Composable
fun MyEditor() {
    val materialTheme = rememberMaterialEditorTheme()
    val session = rememberEditorSession(
        doc = "Hello",
        extensions = basicSetup + materialTheme
    )
    KodeMirror(session = session)
}

This reads colors from MaterialTheme.colorScheme automatically and adapts when switching between light and dark mode. Requires the com.monkopedia.kodemirror:material-theme dependency.

Read-only mode

basicSetup + EditorState.readOnly.of(true)

readOnly vs editable

EditorState.readOnly.of(true) EditorSession.editable (set to false)
Editing Blocked Blocked
Selection Allowed Blocked
Copy Allowed Blocked
Use case Display code that users can select/copy Fully inert display

6. Reacting to changes

Use the onChange convenience to get notified when the document changes:

val session = rememberEditorSession(
    doc = "Hello",
    extensions = basicSetup + onChange { newText ->
        println("Document changed: $newText")
    }
)

For selection changes:

val session = rememberEditorSession(
    doc = "Hello",
    extensions = basicSetup + onSelection { selection ->
        println("Cursor at: ${selection.main.head}")
    }
)

7. Programmatic changes

Use the convenience methods on EditorSession. Document offsets are passed as DocPos (com.monkopedia.kodemirror.state.DocPos), an inline value class over Int that keeps positions from being confused with line numbers or counts:

// Replace entire document
session.setDoc("new content")

// Insert text at a position
session.insertAt(DocPos(0), "// Header\n")

// Delete a range
session.deleteRange(from = DocPos(0), to = DocPos(10))

// Set cursor position
session.select(anchor = DocPos(5))

// Set selection range
session.select(anchor = DocPos(0), head = DocPos(10))

// Select all
session.selectAll()

For more complex changes, use dispatch with a transaction spec:

session.dispatch {
    insert(DocPos(0), "Hello")
    selection(DocPos(5))
    scrollIntoView()
}

8. Sample project

A complete working sample editor is available at samples/editor/ with tab switching between languages and themes.

Next steps