Spaces:
Sleeping
Sleeping
File size: 8,331 Bytes
c63ff03 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
Decorations let you control how to draw or style content in [[Editor extensions|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.
> [!note]
> 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](https://codemirror.net/docs/guide/#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](https://codemirror.net/docs/ref/#view.Decoration%5Emark) style existing elements.
- [Widget decorations](https://codemirror.net/docs/ref/#view.Decoration%5Ewidget) insert elements in the document.
- [Replace decorations](https://codemirror.net/docs/ref/#view.Decoration%5Ereplace) hide or replace part of the document with another element.
- [Line decorations](https://codemirror.net/docs/ref/#view.Decoration%5Eline) 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|state fields]] or _indirectly_ using [[View plugins|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:
1. Use [syntaxTree](https://codemirror.net/docs/ref/#language.syntaxTree) to find list items.
2. 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.
```ts
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](https://codemirror.net/docs/ref/#view.Decoration%5Ereplace).
```ts
const decoration = Decoration.replace({
widget: new EmojiWidget()
});
```
### State fields
To provide decorations from a state field:
1. [[State fields#Defining a state field|Define a state field]] with a `DecorationSet` type.
2. Add the `provide` property to the state field.
```ts
provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
```
```ts
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:
1. [[View plugins#Creating a view plugin|Create a view plugin]].
2. Add a `DecorationSet` member property to your plugin.
3. Initialize the decorations in the `constructor()`.
4. 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.
```ts
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.
|