CodeMirror's extension system provides a powerful plugin architecture for adding functionality to editors. ViewPlugins can manage state, handle events, and integrate with the editor's lifecycle.
class ViewPlugin<T> {
static define<T>(
create: (view: EditorView) => T,
spec?: PluginSpec<T>
): ViewPlugin<T>;
static fromClass<T>(
cls: {new(view: EditorView): T},
spec?: PluginSpec<T>
): ViewPlugin<T>;
}interface PluginValue {
update?(update: ViewUpdate): void;
destroy?(): void;
}interface PluginSpec<T> {
eventHandlers?: DOMEventHandlers<T>;
eventObservers?: DOMEventHandlers<T>;
provide?: (plugin: ViewPlugin<T>) => Extension;
decorations?: (value: T) => DecorationSet;
}class ViewUpdate {
readonly view: EditorView;
readonly state: EditorState;
readonly transactions: readonly Transaction[];
readonly changes: ChangeSet;
readonly startState: EditorState;
// Computed properties
readonly docChanged: boolean;
readonly focusChanged: boolean;
readonly selectionSet: boolean;
readonly viewportChanged: boolean;
readonly heightChanged: boolean;
readonly geometryChanged: boolean;
}type Command = (target: EditorView) => boolean;const clickAddsSelectionRange: Facet<(event: MouseEvent) => boolean>;
const dragMovesSelection: Facet<(event: MouseEvent) => boolean>;
const mouseSelectionStyle: Facet<MakeSelectionStyle>;
const exceptionSink: Facet<(exception: any) => void>;
const updateListener: Facet<(update: ViewUpdate) => void>;
const inputHandler: Facet<(view: EditorView, from: number, to: number, text: string, insert: () => Transaction) => boolean>;
const focusChangeEffect: Facet<(state: EditorState, focusing: boolean) => StateEffect<any> | null>;
const scrollHandler: Facet<(view: EditorView, range: SelectionRange, options: ScrollOptions) => boolean>;function logException(state: EditorState, exception: any, context?: string): void;
const decorations: Facet<DecorationSet>;
const outerDecorations: Facet<DecorationSet>;
const atomicRanges: Facet<(view: EditorView) => DecorationSet>;
const scrollMargins: Facet<(view: EditorView) => Partial<Rect>>;
const styleModule: Facet<StyleModule>;
const contentAttributes: Facet<AttrSource>;
const editorAttributes: Facet<AttrSource>;
const editable: Facet<boolean>;import { ViewPlugin, ViewUpdate, EditorView } from "@codemirror/view";
const myPlugin = ViewPlugin.define((view: EditorView) => {
console.log("Plugin initialized");
return {
update(update: ViewUpdate) {
if (update.docChanged) {
console.log("Document changed");
}
if (update.selectionSet) {
console.log("Selection changed");
}
},
destroy() {
console.log("Plugin destroyed");
}
};
});
// Use the plugin
const view = new EditorView({
state: EditorState.create({
extensions: [myPlugin]
}),
parent: document.body
});const eventHandlerPlugin = ViewPlugin.define(() => ({}), {
eventHandlers: {
mousedown(event, view) {
console.log("Mouse down at", event.clientX, event.clientY);
return false; // Don't prevent default
},
keydown(event, view) {
if (event.key === "Tab") {
// Handle tab key
view.dispatch(view.state.replaceSelection(" "));
return true; // Prevent default
}
return false;
}
}
});import { Decoration, DecorationSet } from "@codemirror/view";
const highlightPlugin = ViewPlugin.define((view) => {
return {
decorations: DecorationSet.empty,
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
},
buildDecorations(view: EditorView) {
const decorations = [];
const text = view.state.doc.toString();
// Highlight all instances of "TODO"
let match;
const regex = /TODO/g;
while ((match = regex.exec(text)) !== null) {
decorations.push(
Decoration.mark({ class: "todo-highlight" })
.range(match.index, match.index + match[0].length)
);
}
return Decoration.set(decorations);
}
};
}, {
decorations: (value) => value.decorations
});class StatusBarPlugin implements PluginValue {
statusBar: HTMLElement;
constructor(view: EditorView) {
this.statusBar = document.createElement("div");
this.statusBar.className = "cm-status-bar";
this.updateStatus(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.selectionSet) {
this.updateStatus(update.view);
}
}
updateStatus(view: EditorView) {
const selection = view.state.selection.main;
const line = view.state.doc.lineAt(selection.head);
this.statusBar.textContent = `Line ${line.number}, Column ${selection.head - line.from + 1}`;
}
destroy() {
this.statusBar.remove();
}
}
const statusBarPlugin = ViewPlugin.fromClass(StatusBarPlugin, {
provide: (plugin) => showPanel.of(() => ({
dom: plugin.value?.statusBar || document.createElement("div"),
top: false
}))
});const insertTimeCommand: Command = (view) => {
const now = new Date().toLocaleTimeString();
const selection = view.state.selection.main;
view.dispatch({
changes: {
from: selection.from,
to: selection.to,
insert: now
},
selection: {
anchor: selection.from + now.length
}
});
return true; // Command succeeded
};
// Use with keymap
const timeKeymap = keymap.of([
{ key: "Ctrl-t", run: insertTimeCommand }
]);const updateLogger = updateListener.of((update: ViewUpdate) => {
console.log("Update:", {
docChanged: update.docChanged,
selectionSet: update.selectionSet,
focusChanged: update.focusChanged,
viewportChanged: update.viewportChanged,
heightChanged: update.heightChanged
});
if (update.transactions.length > 0) {
console.log("Transactions:", update.transactions.length);
}
});const errorHandler = exceptionSink.of((exception) => {
console.error("CodeMirror error:", exception);
// Could send to error reporting service
});
// Log an exception manually
logException(view.state, new Error("Something went wrong"), "my-plugin");const customInputHandler = inputHandler.of((view, from, to, text, insert) => {
// Handle auto-closing brackets
if (text === "(") {
view.dispatch({
changes: [{from, to, insert: "()"}],
selection: {anchor: from + 1}
});
return true; // Handled
}
return false; // Let default handler run
});