Customize the editor

Inkdrop's text editor is built on CodeMirror, which is customized through composable extensions — keymaps, view plugins, facets, themes. A plugin builds an extension, registers it through a command, and Inkdrop applies it to every editor it creates.

Use CodeMirror packages without bundling them

Inkdrop ships the @codemirror/* packages and provides them to plugins at runtime, so import them as usual but keep them out of your bundle: add the ones you need as devDependencies and mark them external in your build config, together with the inkdrop module:

import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['src/index.ts'],
  outDir: 'lib',
  format: ['cjs'],
  outExtensions: () => ({ js: '.js' }),
  deps: {
    neverBundle: ['inkdrop', /^@codemirror\//]
  }
})

Third-party CodeMirror packages (e.g. @replit/codemirror-vim) are not provided by the app — let your bundler include those. (Example: vim)

Registering an extension

Build an extension and dispatch editor:add-extension. Registered extensions are kept in the app state and applied to every editor — including editors created later as the user switches notes — so you register once in activate() and remove in deactivate():

import type { Extension } from '@codemirror/state'
import { highlightTrailingWhitespace } from '@codemirror/view'
import type { Environment, IInkdropPlugin } from '@inkdropapp/types'

import { getEnv, setEnv } from './env'

class MyPlugin implements IInkdropPlugin {
  extension: Extension | null = null

  activate(env: Environment) {
    setEnv(env)
    this.extension = highlightTrailingWhitespace()
    env.ensureEditorLoaded(() => {
      env.commands.dispatch(document.body, 'editor:add-extension', {
        extension: this.extension
      })
    })
  }

  deactivate() {
    getEnv().commands.dispatch(document.body, 'editor:remove-extension', {
      extension: this.extension
    })
    this.extension = null
    setEnv(undefined)
  }
}

export default new MyPlugin()

setEnv / getEnv is the small helper that captures the Environment handed to activate() so your plugin's other modules can reach it — the Word Count plugin walkthrough shows the helper itself.

Two details worth knowing:

  • Removal is by identity. editor:remove-extension removes the exact object you added — keep the reference around, as the extension field above does. Every highlightTrailingWhitespace() call returns a fresh instance, so a rebuilt extension would not match.
  • Adding the same reference twice is a no-op, so dispatching again (e.g. when a config option toggles a feature back on) is harmless.

For a real-world example, the official vim plugin wires @replit/codemirror-vim into the editor exactly this way — and toggles an optional relative-line-numbers extension by add/remove-dispatching it whenever its config value changes.

Adding a code-block language

Code blocks are highlighted by language. To register an additional language, dispatch editor:add-code-language with a LanguageDescription:

import { LanguageDescription } from '@codemirror/language'

const myLang = LanguageDescription.of({
  name: 'mylang',
  extensions: ['mylang'],
  load: async () => {
    const { myLanguageSupport } = await import('./language.js')
    return myLanguageSupport()
  }
})

env.commands.dispatch(document.body, 'editor:add-code-language', {
  lang: myLang
})

Remove it in deactivate() with editor:remove-code-language, which takes the language name.

Extending the Markdown syntax

The Markdown parser itself is a Lezer grammar and accepts MarkdownExtensions from @lezer/markdown. Register one with editor:add-markdown-extension to support custom Markdown syntax in the editor.

Toggling editor options

Line numbers, line wrapping and readable line width are user settings rather than CodeMirror options. Flip them through the Configenv.config.set('editor.lineNumbers', true) — or dispatch the matching commands: editor:toggle-line-numbers, editor:toggle-line-wrapping.

Keymaps and styling

Key bindings belong in Inkdrop's keymap system, not in a CodeMirror keymap.of(…) extension — bindings shipped as JSON are visible in the Keybindings preferences pane and can be overridden by the user's keymap.json, while a CodeMirror keymap bypasses both. Register a command and bind it on the editor with the .cm-editor selector:

{
  ".cm-editor": {
    "ctrl-shift-down": "editor:select-lines-downward"
  }
}

For styling, target .cm-editor and the .tok-* syntax token classes — see Creating a theme and Style tweaks.

Can you help us improve the docs? 🙏

The source of these docs is here on GitHub. If you see a way these docs can be improved, please fork us!