Skip to content

CodeMirror extensions

The Editor API exposes a document abstraction for commands and transforms. When you need syntax-aware editing, live decorations, or custom autocomplete, register CodeMirror 6 extensions through the plugin API.

Prefer these registration helpers over reaching into EditorView instances directly. They keep extensions scoped, reload-safe, and compatible with reading mode transitions.

Most language and markdown extensions register against a specific view type:

this.registerEditorExtension(markupEditor(javascript()), "javascript");
this.registerEditorExtension(myTableExtension, "markdown");

registerEditorExtension() on Plugin forwards to the app registry and automatically unregisters on plugin unload.

Register against all editors when the behavior is view-agnostic:

this.app.registerEditorExtension(myExtension);

Use global registration sparingly. Scoped registration avoids affecting canvas, search, or other specialized editors unintentionally.

When registrations change, the workspace re-applies extensions to open editors through workspace.updateOptions().

The API provides markupEditor() to bundle common editor extension groups—language parser, highlighting, and optional language-service adapters:

import { markupEditor } from "@lapis-notes/api/editor";
import { javascript } from "@codemirror/lang-javascript";
import { languageServiceExtensions } from "@lapis-notes/api/editor/language-service";
this.registerEditorExtension(
markupEditor(
javascript({ typescript: true }),
languageServiceExtensions({ languageId: "typescript" }),
),
"typescript",
);

Compose additional CodeMirror extensions as arguments when you need plugin-specific behavior on top of the base language stack.

For Obsidian-style popover suggestions, subclass EditorSuggest and register it against a view type:

import { EditorSuggest } from "@lapis-notes/api";
class MySuggest extends EditorSuggest {
onTrigger(cursor, editor) {
// Return trigger info or null when inactive.
}
getSuggestions(context) {
// Return suggestion items.
}
}
this.registerEditorSuggest(new MySuggest(this.app), "markdown");

EditorSuggest integrates with the shared popover suggest UI. Use it when suggestions are contextual to cursor position and should appear in a floating menu rather than inline autocomplete.

The notebook plugin registers NotebookCellSuggest for markdown cells this way.

Every imperative registration must unwind on unload:

APICleanup
registerEditorExtension()Automatic through Plugin.register()
registerEditorSuggest()Automatic through Plugin.register()
app.registerEditorExtension()Manual — prefer Plugin helpers
Event listeners and intervalsUse this.register(() => ...)

Failing to unregister extensions causes duplicate widgets, stale diagnostics, or memory leaks after disabling a plugin.

Good fits:

  • Syntax decorations and widgets (headings, task checkboxes, live-preview blocks)
  • Input handlers and keymaps for structured editing (tables, lists)
  • Vault-aware autocomplete that does not need a project-wide analyzer
  • View-specific formatting or fold behavior

Poor fits:

  • Cross-file type checking — use a language service instead
  • Long-running analysis on every keystroke — move work to a worker provider
  • Direct DOM mutation outside CodeMirror’s update cycle

Study these first-party plugins:

PluginWhat to learn
MarkdownLive preview decorations, wiki links, tables, completion, nested code-block highlighting
LangCodeLanguage packages, markupEditor(), languageServiceExtensions()
SearchCustom Lezer grammar, query autocomplete, parse diagnostics
BasesSQL highlighting and keyword completion in query editors
NotebookEditorSuggest for cell-aware suggestions

Use the editor abstraction (editor.replaceRange, editor.getCursor, command callbacks) when manipulating document text from commands.

Use CodeMirror extensions when participating in the editing pipeline: token classes, widgets, autocomplete sources, or lint sources bridged from language services.

Do not depend on undocumented CodeMirror internals or private workspace editor fields. If a use case needs new editor surface area, prefer a language-service provider or a documented registration API.