extend/overlay mode

In CM5 I could overlay or extend a mode by either using the overlayMode function or wrapping it. Is this something that’s possible with CM6? My use-case was adding some custom stuff to the GFM mode. I saw that there’s a lang-markdown package and also a stream parser (which is what I use in CM5) but I’m not exactly sure if I can use them together or if there’s a preferred approach. Sorry If I’ve missed this in the documentation.

Thanks!

Overlay-style highlighting is now done with a view plugin that maintains a set of decorations for visible content. Extending or wrapping a mode isn’t really possible anymore. Which feature, specifically, do you want to add? The new Markdown parser was written with the intention of, at some point, allowing extension through user code, but I haven’t nailed down the API for that yet.

My use case is adding some non-standard markdown syntax like @ style mention, :: highlighting :: ,#hashtags and so on which was pretty straightforward by using Codemirror.defineMode and the stream tokenizer and then using overlayMode to combine with the normal GFM mode.

If these aren’t going to affect the Markdown parsing itself, and don’t need to appear in the syntax tree, a crude view plugin like below should work. If you want to actually integrate these into the Markdown parser, you might need a lot more patience (or could consider paying me to implement that feature).

let plugin = ViewPlugin.fromClass(class {
  decorations: DecorationSet
  constructor(view: EditorView) {
    this.decorations = this.mkDeco(view)
  }
  update(update: ViewUpdate) {
    if (update.viewportChanged || update.docChanged) this.decorations = this.mkDeco(update.view)
  }
  mkDeco(view: EditorView) {
    let b = new RangeSetBuilder<Decoration>()
    let highlight = /(@\w+)|(::.*?::)|(#\w+)/g
    for (let {from, to} of view.visibleRanges) {
      let range = view.state.sliceDoc(from, to), m
      while (m = highlight.exec(range))
        b.add(from + m.index, from + m.index + m[0].length, Decoration.mark({
          class: m[1] ? "at-mention" : m[2] ? "highlight" : "hashtag"
        }))
    }
    return b.finish()
  }
}, {
  decorations: v => v.decorations
})

Thanks a lot! I’ll test it out.

Given how verbose my previous answer was, I’ve added a helper class to @codemirror/view 0.17.4 that makes this easier…

import {MatchDecorator, ViewPlugin, Decoration} from "@codemirror/view"

let mentionDeco = Decoration.mark({class: "mention"})
let tagDeco = Decoration.mark({class: "hashtag"})
let highlightDeco = Decoration.mark({class: "highlight"})
let decorator = new MatchDecorator({
  regexp: /(@\w+)|(::.*?::)|(#\w+)/g,
  decoration: m => m[1] ? mentionDeco : m[2] ? highlightDeco : tagDeco
})

let plugin = ViewPlugin.define(view => ({
  decorations: decorator.createDeco(view),
  update(u) { this.decorations = decorator.updateDeco(u, this.decorations) }
}), {
  decorations: v => v.decorations
})
2 Likes

Excuse me, after creating a Plugin through ViewPlugin.define, how to register it to EditorView for work

Excuse me, after creating a Plugin through ViewPlugin.define, how to register it to EditorView for work

Thanks for providing this example, it already helped me a lot! It assigns regular CSS classes to the matches, however I’d prefer to assign style tags, as defined in @codemirror/highlight instead. Is this possible or do I have to take a different approach for that?

That wasn’t easily possible before, but the new HighlightStyle.get function should be useful here.

(Also, in the context of the original question in this thread—the Markdown parser can now be extended, which may be a neater way of solving this than an overlay.)

1 Like

CodeMirror plugin crashed: RangeError: Widget decorations can only have zero-length ranges.

In this way, an error is reported when entering the full angle symbol, such as:;’

from:
this.decorations = decorator.updateDeco(u, this.decorations);

class FieldWidget extends WidgetType {
  public toDOM(): HTMLElement {
    const el = document.createElement('span');
    el.classList.add('cm-field');
    el.innerHTML = 'test';
    return el;
  }
}

let decorator = new MatchDecorator({
  regexp: /(@\w+)|(::.*?::)|(#\w+)/g,
  decoration: Decoration.widget({
    widget: new FieldWidget()
  })
});
let plugin = ViewPlugin.define(view => ({
  decorations: decorator.createDeco(view),
  update(u) { if (u.viewportChanged) this.decorations = decorator.updateDeco(u, this.decorations) }
}), {
  decorations: v => v.decorations
})

Just add this line. if (u.viewportChanged). No error will be reported

As the error mentions, you can’t use widget decorations like this. You probably want Decoration.replace.

I’m feeling a little stupid asking this question: Can I define multiple MatchDecorators within one ViewPlugin?

let decorateEmail = new MatchDecorator({
  regexp: /…/g,
  decoration: …
})

let decorateUrl = new MatchDecorator({
  regexp: /…/g,
  decoration: …
})

let plugin = ViewPlugin.define(view => ({
  decorations: // How to assign multiple decorations here?
  update(u) { this.decorations = … // And here. }
}), {
  decorations: v => v.decorations // And here?
})

Store the decorations in different properties, and use something like provide: [PluginField.decorations.from(v => v.field1), PluginField.decorations.from(v => v.field2)] instead of the decorations option.

1 Like