# Customize the editor

Source: https://developers.inkdrop.app/guides/customize-the-editor

Inkdrop's text editor is built on [CodeMirror](https://codemirror.net/), which is customized through composable [extensions](https://codemirror.net/docs/ref/#state.Extension) — 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:

```js filename="tsdown.config.js"
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](https://github.com/inkdropapp/inkdrop-vim))

## Registering an extension

Build an extension and dispatch [`editor:add-extension`](https://developers.inkdrop.app/guides/list-of-commands#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()`:

```ts filename="src/index.ts"
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](https://developers.inkdrop.app/modules/environment) handed to `activate()` so your plugin's other modules can reach it — the [Word Count plugin walkthrough](https://developers.inkdrop.app/guides/plugin-word-count#understanding-the-generated-code) 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](https://github.com/inkdropapp/inkdrop-vim) 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`](https://developers.inkdrop.app/guides/list-of-commands#editor-add-code-language) with a [LanguageDescription](https://codemirror.net/docs/ref/#language.LanguageDescription):

```ts
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`](https://developers.inkdrop.app/guides/list-of-commands#editor-remove-code-language), which takes the language `name`.

## Extending the Markdown syntax

The Markdown parser itself is a [Lezer](https://lezer.codemirror.net/) grammar and accepts `MarkdownExtension`s from [@lezer/markdown](https://github.com/lezer-parser/markdown). Register one with [`editor:add-markdown-extension`](https://developers.inkdrop.app/guides/list-of-commands#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 [Config](https://developers.inkdrop.app/modules/config) — `env.config.set('editor.lineNumbers', true)` — or dispatch the matching commands: [`editor:toggle-line-numbers`](https://developers.inkdrop.app/guides/list-of-commands#editor-toggle-line-numbers), [`editor:toggle-line-wrapping`](https://developers.inkdrop.app/guides/list-of-commands#editor-toggle-line-wrapping).

## Keymaps and styling

Key bindings belong in Inkdrop's [keymap system](https://developers.inkdrop.app/modules/keymap-manager), 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](https://developers.inkdrop.app/modules/command-registry) and bind it on the editor with the `.cm-editor` selector:

```json filename="keymaps/my-plugin.json"
{
  ".cm-editor": {
    "ctrl-shift-down": "editor:select-lines-downward"
  }
}
```

For styling, target `.cm-editor` and the `.tok-*` syntax token classes — see [Creating a theme](https://developers.inkdrop.app/guides/create-a-theme) and [Style tweaks](https://developers.inkdrop.app/guides/style-tweaks).
