Edit: This code is working using the Decoration.line
method like the Zebra example. I don’t understand why it wasn’t but after a short walk it just worked again. The following is no longer relavent but kept for prosperity.
I apologize for the newbie nature of this question but I have reached an impasse with two days of research into the docs and examples.
I am working on a plugin that will decorate each line based on a validation function. Essentially as the user types (or the content changes) I want to highlight valid lines as green and invalid lines as red (and blank lines as normal styling).
At first I thought this didn’t seem like a language syntax highlighting as the result isn’t about the syntax of the content but if some other function returns a boolean response based on that line or not.
I then thought this was a Decoration.line
but thinking about it maybe this isn’t how the system works as lines decorations are more about the editor then the text within. See zebra stripes example which focuses on the lines as numerical calculations and not the textual content within.
Then I was thinking maybe this is about marks but my ability to use them has really confused me.
As an example I was able to make a gutter plugin that reacts to the content (see code snippet below) but I was not able to make a decoration for this. Specifically following the zebra stripes example I found that the update()
method never gets called after the user changes the content. Thus lines and marks only seem to be useful for first render.
Gutter example
interface Validator {
(text: string): string | true;
}
export class ValidityGutter {
constructor(private validate: Validator) {}
extension() {
const validMarker = new ValidityGutter.ValidMarker();
const invalidMarker = new ValidityGutter.InvalidMarker();
const blankMarker = new ValidityGutter.BlankMarker();
return gutter({
initialSpacer: () => blankMarker,
lineMarker: (view, line) => {
let { text } = view.state.doc.lineAt(line.from);
if (text.trim() === '') return blankMarker;
return this.validate(text) === true ? validMarker : invalidMarker;
},
});
}
static ValidMarker = class extends GutterMarker {
toDOM() {
let el = document.createElement('span');
el.style.color = 'green';
el.innerHTML = ' ✔︎ ';
return el;
}
};
static InvalidMarker = class extends GutterMarker {
toDOM() {
let el = document.createElement('span');
el.style.color = 'red';
el.innerHTML = ' ✗ ︎';
return el;
}
};
static BlankMarker = class extends GutterMarker {
toDOM() {
let el = document.createElement('span');
el.innerHTML = ' ︎';
return el;
}
};
}
// Usage:
new EditorView({
…,
extensions: [
minimalSetup,
new ValidityGutter((text: string) => /foo/.test(text)).extension(),
],
});
Decorator example
interface Validator {
(text: string): string | true;
}
class ValidityDecoratorPlugin implements PluginValue {
decorations: DecorationSet;
constructor(private validate: Validator, view: EditorView) {
this.decorations = this.decorate(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) this.decorations = this.decorate(update.view);
}
private decorate(view: EditorView) {
let { validLine, invalidLine } = ValidityDecoratorPlugin;
let builder = new RangeSetBuilder<Decoration>();
const checkValidity = (line: Line) =>
this.validate(line.text) === true ? validLine : invalidLine;
const isBlankLine = (line: Line) => line.text.trim() === '';
function* textPositions() {
for (let { from, to } of view.visibleRanges) {
for (let pos = from; pos <= to; ) {
let line = view.state.doc.lineAt(pos);
yield line;
pos = line.to + 1;
}
}
}
for (let line of textPositions())
if (!isBlankLine(line)) builder.add(line.from, line.from, checkValidity(line));
return builder.finish();
}
static validLine = Decoration.line({
attributes: { 'data-validity': 'valid' },
});
static invalidLine = Decoration.line({
attributes: { 'data-validity': 'invalid' },
});
}
class ValidityDecorator {
constructor(private validate: Validator) {}
extension() {
const factory = (view: EditorView) => new ValidityDecoratorExtension(this.validate, view);
return ViewPlugin.define(factory, { decorations: (v) => v.decorations });
}
}
// Usage:
new EditorView({
…,
extensions: [
minimalSetup,
new ValidityDecorator((text: string) => /foo/.test(text)).extension(),
],
});
Any help or advise would be very much appreciated. Thank you.