Decorations
Decorations let you control how to draw or style content in editor extensions. If you intend to change the look and feel by adding, replacing, or styling elements in the editor, you most likely need to use decorations.
By the end of this page, you'll be able to:
- Understand how to use decorations to change the editor appearance.
- Understand the difference between providing decoration using state fields and view plugins.
This page aims to distill the official CodeMirror 6 documentation for Obsidian plugin developers. For more detailed information on state fields, refer to Decorating the Document.
Prerequisites
- Basic understanding of state fields.
- Basic understanding of view plugins.
Overview
Without decorations, the document would render as plain text. Not very interesting at all. Using decorations, you can change how to display the document, for example by highlighting text or adding custom HTML elements.
You can use the following types of decorations:
- Mark decorations style existing elements.
- Widget decorations insert elements in the document.
- Replace decorations hide or replace part of the document with another element.
- Line decorations add styling to the lines, rather than the document itself.
To use decorations, you need to create them inside an editor extension and have the extension provide them to the editor. You can provide decorations to the editor in two ways, either directly using state fields or indirectly using view plugins.
Should I use a view plugin or a state field?
Both view plugins and state fields can provide decorations to the editor, but they have some differences.
- Use a view plugin if you can determine the decoration based on what's inside the viewport.
- Use a state field if you need to manage decorations outside of the viewport.
- Use a state field if you want to make changes that could change the content of the viewport, for example by adding line breaks.
If you can implement your extension using either approach, then the view plugin generally results in better performance. For example, imagine that you want to implement an editor extension that checks the spelling of a document.
One way would be to pass the entire document to an external spell checker which then returns a list of spelling errors. In this case, you'd need to map each error to a decoration and use a state field to manage decorations regardless of what's in the viewport at the moment.
Another way would be to only spellcheck what's visible in the viewport. The extension would need to continuously run a spell check as the user scrolls through the document, but you'd be able to spell check documents with millions of lines of text.
Providing decorations
Imagine that you want to build an editor extension that replaces the bullet list item with an emoji. You can accomplish this with either a view plugin or a state field, with some differences. In this section, you'll see how to implement it with both types of extensions.
Both implementations share the same core logic:
- Use syntaxTree to find list items.
- For every list item, replace leading hyphens,
-
, with a widget.
Widgets
Widgets are custom HTML elements that you can add to the editor. You can either insert a widget at a specific position in the document, or replace a piece of content with a widget.
The following example defines a widget that returns an HTML element, <span>👉</span>
. You'll use this widget later on.
import { EditorView, WidgetType } from "@codemirror/view";
export class EmojiWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement {
const div = document.createElement("span");
div.innerText = "👉";
return div;
}
}
To replace a range of content in your document with the emoji widget, use the replace decoration.
const decoration = Decoration.replace({
widget: new EmojiWidget()
});
State fields
To provide decorations from a state field:
Define a state field with a
DecorationSet
type.Add the
provide
property to the state field.provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
import { syntaxTree } from "@codemirror/language";
import {
Extension,
RangeSetBuilder,
StateField,
Transaction,
} from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
WidgetType,
} from "@codemirror/view";
import { EmojiWidget } from "emoji";
export const emojiListField = StateField.define<DecorationSet>({
create(state): DecorationSet {
return Decoration.none;
},
update(oldState: DecorationSet, transaction: Transaction): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
syntaxTree(transaction.state).iterate({
enter(node) {
if (node.type.name.startsWith("list")) {
// Position of the '-' or the '*'.
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
return builder.finish();
},
provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
});
View plugins
To manage your decorations using a view plugin:
- Create a view plugin.
- Add a
DecorationSet
member property to your plugin. - Initialize the decorations in the
constructor()
. - Rebuild decorations in
update()
.
Not all updates are reasons to rebuild your decorations. The following example only rebuilds decorations whenever the underlying document or the viewport changes.
import { syntaxTree } from "@codemirror/language";
import { RangeSetBuilder } from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
PluginSpec,
PluginValue,
ViewPlugin,
ViewUpdate,
WidgetType,
} from "@codemirror/view";
import { EmojiWidget } from "emoji";
class EmojiListPlugin implements PluginValue {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
destroy() {}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.type.name.startsWith("list")) {
// Position of the '-' or the '*'.
const listCharFrom = node.from - 2;
builder.add(
listCharFrom,
listCharFrom + 1,
Decoration.replace({
widget: new EmojiWidget(),
})
);
}
},
});
}
return builder.finish();
}
}
const pluginSpec: PluginSpec<EmojiListPlugin> = {
decorations: (value: EmojiListPlugin) => value.decorations,
};
export const emojiListPlugin = ViewPlugin.fromClass(
EmojiListPlugin,
pluginSpec
);
buildDecorations()
is a helper method that builds a complete set of decorations based on the editor view.
Notice the second argument to the ViewPlugin.fromClass()
function. The decorations
property in the PluginSpec
specifies how the view plugin provides the decorations to the editor.
Since the view plugin knows what's visible to the user, you can use view.visibleRanges
to limit what parts of the syntax tree to visit.