How do I style lines without a language syntax? Newbie Question

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.

Looks like I might have spoke too soon. I used the zebra mod instead of the validate function and that worked. So I have a logic bug in my code. Thank you for the rubber duck.

Yup this is totally working now. I guess all I had to do was post here. Sorry for the pointless post. Maybe someone will find the code inspiring or something.