DOM view component for the CodeMirror code editor
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
The decoration system provides flexible styling and widget insertion capabilities for CodeMirror editors. Decorations can mark text ranges, insert widgets, replace content, and style entire lines.
abstract class Decoration {
static mark(spec: MarkDecorationSpec): Decoration;
static widget(spec: WidgetDecorationSpec): Decoration;
static replace(spec: ReplaceDecorationSpec): Decoration;
static line(spec: LineDecorationSpec): Decoration;
readonly spec: any;
readonly startSide: number;
readonly endSide: number;
readonly point: boolean;
range(from: number, to?: number): Range<Decoration>;
}class DecorationSet extends RangeSet<Decoration> {
static readonly empty: DecorationSet;
static create(view: EditorView, decorations: Range<Decoration>[] | readonly Range<Decoration>[]): DecorationSet;
update(mapping: ChangeDesc, filterFrom?: number, filterTo?: number): DecorationSet;
map(mapping: ChangeDesc, start?: number, end?: number): DecorationSet;
}interface MarkDecorationSpec {
inclusive?: boolean;
inclusiveStart?: boolean;
inclusiveEnd?: boolean;
attributes?: {[key: string]: string};
class?: string;
tagName?: string;
bidiIsolate?: Direction | null;
[other: string]: any;
}interface WidgetDecorationSpec {
widget: WidgetType;
side?: number;
block?: boolean;
inlineOrder?: boolean;
[other: string]: any;
}abstract class WidgetType {
abstract toDOM(view: EditorView): HTMLElement;
abstract eq(other: WidgetType): boolean;
updateDOM?(dom: HTMLElement, view: EditorView): boolean;
get estimatedHeight(): number;
ignoreEvent?(event: Event): boolean;
coordsAt?(dom: HTMLElement, pos: number, side: number): Rect | null;
get isHidden(): boolean;
destroy?(dom: HTMLElement): void;
}interface ReplaceDecorationSpec {
widget?: WidgetType;
inclusive?: boolean;
inclusiveStart?: boolean;
inclusiveEnd?: boolean;
block?: boolean;
[other: string]: any;
}interface LineDecorationSpec {
attributes?: {[key: string]: string};
class?: string;
[other: string]: any;
}abstract class BlockType {
abstract toDOM(view: EditorView, pos: number): HTMLElement;
abstract updateDOM(dom: HTMLElement, view: EditorView, pos: number): boolean;
get side(): number;
get editable(): boolean;
get estimatedHeight(): number;
get lineBreaks(): number;
}import { Decoration, DecorationSet, ViewPlugin } from "@codemirror/view";
// Highlight all instances of a word
const highlightWord = (word: string, className: string) => {
return ViewPlugin.define((view) => {
return {
decorations: buildHighlights(view, word, className),
update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = buildHighlights(update.view, word, className);
}
}
};
}, {
decorations: v => v.decorations
});
};
function buildHighlights(view: EditorView, word: string, className: string) {
const decorations = [];
const regex = new RegExp(word, 'gi');
const text = view.state.doc.toString();
let match;
while ((match = regex.exec(text)) !== null) {
decorations.push(
Decoration.mark({ class: className })
.range(match.index, match.index + match[0].length)
);
}
return Decoration.set(decorations);
}// Highlight error lines
const errorLines = [5, 12, 18]; // Line numbers with errors
const errorDecorations = errorLines.map(lineNumber => {
const line = view.state.doc.line(lineNumber);
return Decoration.line({ class: "error-line" }).range(line.from);
});
const errorHighlights = Decoration.set(errorDecorations);class LineWidget extends WidgetType {
constructor(private message: string) {
super();
}
toDOM(view: EditorView): HTMLElement {
const div = document.createElement("div");
div.className = "line-widget";
div.textContent = this.message;
return div;
}
eq(other: WidgetType): boolean {
return other instanceof LineWidget && other.message === this.message;
}
get estimatedHeight() {
return 20; // Height in pixels
}
}
// Add widget after a line
const line = view.state.doc.line(3);
const widget = Decoration.widget({
widget: new LineWidget("This is a line widget"),
side: 1,
block: true
}).range(line.to);class InlineButton extends WidgetType {
constructor(private text: string, private onClick: () => void) {
super();
}
toDOM(view: EditorView): HTMLElement {
const button = document.createElement("button");
button.textContent = this.text;
button.onclick = this.onClick;
return button;
}
eq(other: WidgetType): boolean {
return other instanceof InlineButton && other.text === this.text;
}
ignoreEvent(event: Event): boolean {
return event.type !== "mousedown";
}
}
// Insert inline widget at cursor
const pos = view.state.selection.main.head;
const inlineWidget = Decoration.widget({
widget: new InlineButton("Click me", () => alert("Clicked!")),
side: 0
}).range(pos);class FoldWidget extends WidgetType {
constructor(private foldedText: string) {
super();
}
toDOM(view: EditorView): HTMLElement {
const span = document.createElement("span");
span.className = "cm-fold-placeholder";
span.textContent = `[${this.foldedText}]`;
span.title = "Click to unfold";
return span;
}
eq(other: WidgetType): boolean {
return other instanceof FoldWidget && other.foldedText === this.foldedText;
}
}
// Replace text range with fold widget
const foldDecoration = Decoration.replace({
widget: new FoldWidget("..."),
inclusive: true
}).range(from, to);// Add custom attributes to marked text
const linkDecoration = Decoration.mark({
tagName: "a",
attributes: {
href: "https://example.com",
target: "_blank",
title: "External link"
},
class: "external-link"
});
// Apply to a range
const decoratedRange = linkDecoration.range(from, to);// Isolate RTL text in LTR context
const rtlDecoration = Decoration.mark({
bidiIsolate: Direction.RTL,
class: "rtl-text"
}).range(from, to);const dynamicDecorations = ViewPlugin.define((view) => {
let decorations = DecorationSet.empty;
return {
decorations,
update(update) {
if (update.docChanged) {
// Map existing decorations through changes
decorations = decorations.map(update.changes);
// Add new decorations based on content
const newDecos = findAndDecorate(update.view);
decorations = decorations.update({
add: newDecos,
filter: (from, to) => {
// Remove decorations that are no longer valid
return isStillValid(from, to, update.view);
}
});
}
}
};
}, {
decorations: v => v.decorations
});// Different styles based on context
const contextualMark = (type: "warning" | "error" | "info") => {
const classMap = {
warning: "text-warning",
error: "text-error",
info: "text-info"
};
return Decoration.mark({
class: classMap[type],
attributes: { "data-severity": type }
});
};
// Apply different decorations
const warningRange = contextualMark("warning").range(10, 20);
const errorRange = contextualMark("error").range(30, 40);// Efficient decoration building for large documents
function buildDecorationsEfficiently(view: EditorView) {
const decorations = [];
const { from, to } = view.viewport;
// Only process visible range
const visibleText = view.state.sliceDoc(from, to);
let match;
const regex = /pattern/g;
while ((match = regex.exec(visibleText)) !== null) {
const start = from + match.index;
const end = start + match[0].length;
decorations.push(
Decoration.mark({ class: "highlight" }).range(start, end)
);
}
return Decoration.set(decorations);
}Automatic pattern matching and decoration system for finding and styling regular expression matches.
class MatchDecorator {
constructor(config: MatchDecoratorConfig);
createDeco(view: EditorView): DecorationSet;
}
interface MatchDecoratorConfig {
regexp: RegExp;
decoration?: Decoration | ((match: RegExpExecArray, view: EditorView, pos: number) => Decoration);
decorate?: (add: (from: number, to: number, decoration: Decoration) => void, from: number, to: number, match: RegExpExecArray, view: EditorView) => void;
boundary?: RegExp;
}import { MatchDecorator, Decoration, ViewPlugin } from "@codemirror/view";
// Highlight URLs automatically
const urlMatcher = new MatchDecorator({
regexp: /https?:\/\/[^\s]+/g,
decoration: Decoration.mark({
class: "url-link",
tagName: "a",
attributes: {
href: match => match[0],
target: "_blank"
}
})
});
const urlPlugin = ViewPlugin.define((view) => ({
decorations: urlMatcher.createDeco(view),
update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = urlMatcher.createDeco(update.view);
}
}
}), {
decorations: v => v.decorations
});
// Dynamic decoration based on match content
const todoMatcher = new MatchDecorator({
regexp: /TODO:\s*(.+)/g,
decorate: (add, from, to, match, view) => {
const priority = match[1].includes("URGENT") ? "high" : "normal";
add(from, to, Decoration.mark({
class: `todo-item priority-${priority}`,
attributes: { "data-todo": match[1] }
}));
}
});