I’m having problems trying to add decorations (highlights) to my CodeMirror editor (I’m using version 6).
I need the editor to match certain regex patterns and insert an element with some text instead of the matched pattern. The inserted elements must be completely deleted when pressing Backspace infront of them and if they could be selectable, even better.
Example: Regex /%d/ is replaced with the text “string” inside a span element.
This example should get you mostly there. Making widgets selectable is not something that the library will handle for you, but the general approach there would be to add a state field that tracks if a widget is selected and emits an additional decoration around it to make it recognizeable as such, and some mouse/key handlers that set this state (along with the real selection) when at the appropriate times.
Thank you! Where is the PlaceholderWidget type imported from in that example? And how would I have multiple regex instead of just handling one case, as the example shows?
I’m using TypeScript and “decorations:” gives the error “Type ‘(instance: (Anonymous class)) => DecorationSet’ is not assignable to type ‘(value: (Anonymous class)) => DecorationSet’.
Type ‘DecorationSet’ is missing the following properties from type ‘RangeSet’: size, update, between, iterts(2322)” and “view =>” gives the error “Argument of type ‘(view: EditorView) => DecorationSet | DecorationSet’ is not assignable to parameter of type ‘(view: EditorView) => RangeSet’.
Type ‘DecorationSet | DecorationSet’ is not assignable to type ‘RangeSet’.ts(2345)”.
Both EditorView.decorations and EditorView.atomicRanges expect a single decoration set, not an array of them. You could do something like mapping over matchDecorators and adding an accessor for every element in the array in the provide result, but I think this is going to be easier (and more efficient) if you create a combined regexp that matches all of the patterns, and checks the groups in the match result to determine what kind of widget to return, so that you have only one decoration set for these.
Thank you so much, I think I’ll go with your last suggestion. One last question, if you don’t mind. How can I make these placeholders (decorators) removable in the editor? Nothing seems to happen when I press backspace/delete on them
I was missing the minimal setup (CodeMirror Reference Manual), my bad. Do you have any examples of how I could make tooltips for these placeholder, so that when they’re hovered a tooltip will appear with some text?
I’m having a hard time getting the tooltip to work with the placeholders. It does work, but only if either side of the placeholder element is hovered, not the middle of the element.
Do you have any idea of why that is?
Here’s my code, where tooltipExtension is the function that handles the tooltips:
class PlaceholderWidget extends WidgetType {
value: string;
constructor(value: string) {
super();
this.value = value;
}
toDOM(): HTMLElement {
const span = document.createElement('span');
span.textContent = this.value;
span.className = 'bg-red-400 rounded text-white px-1 py-0.5';
return span;
}
}
const placeholderMatcher = new MatchDecorator({
regexp: /%(d|s)/g,
decoration: match => Decoration.replace({
widget: new PlaceholderWidget(match[0]),
}),
});
const placeholderExtension = ViewPlugin.fromClass(class {
placeholders: DecorationSet
constructor(view: EditorView) {
this.placeholders = placeholderMatcher.createDeco(view);
}
update(update: ViewUpdate) {
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
}
}, {
decorations: instance => instance.placeholders,
provide: plugin => EditorView.atomicRanges.of(view => {
return view.plugin(plugin)?.placeholders || Decoration.none;
}),
});
const tooltipExtension = hoverTooltip((view, pos, side) => {
let {from, to, text} = view.state.doc.lineAt(pos);
let regex = /%(d|s)/g, match;
let found: any = null;
while (match = regex.exec(text)) {
let start = from + match.index;
let end = start + match[0].length;
if (pos >= start && pos <= end) {
found = {start, end};
break;
}
}
if (!found) return null;
if (found.start == pos && side < 0 || found.end == pos && side > 0) return null;
return {
pos: found.start,
end: found.end,
above: true,
create(view) {
let dom = document.createElement("div");
dom.textContent = text.slice(found.start - from, found.end - from);
return {dom};
}
};
});