Decorating newline characters

I’d like to make newline characters (\n) visible. I tried to adapt highlightWhitespace as follows:

function matcher(decorator: MatchDecorator): Extension {
    return ViewPlugin.define((view) => ({
        decorations: decorator.createDeco(view),
        update(u): void {
            this.decorations = decorator.updateDeco(u, this.decorations);
        },
    }), {
        decorations: (v) => v.decorations,
    });
}

const newlineHighlighter = matcher(
    new MatchDecorator({
        regexp: /\n/g,
        decoration: (match) => Decoration.mark({ class: 'cm-newline' }),
    }),
);

However this doesn’t work … the documentation states for regexp:

The regular expression to match against the content. Will only be matched inside lines (not across them). Should have its ‘g’ flag set.

I assume this doesn’t work because the newline isn’t considered to be part of the line? How could I make this work?

Indeed, line breaks aren’t part f the text. You could write an extension that adds a widget to every line except the last line to get something like this, I suppose.

Ah ok thanks … using widget decorators indeed works :slight_smile:

(sidenote: I tried using an empty mark decoration but apparently mark decorations cannot be empty … hence a widget decoration)

highlightNewLine extension

import type { DecorationSet, EditorView, ViewUpdate } from '@codemirror/view'
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view'

class NewlineWidget extends WidgetType {
  toDOM() {
    const span = document.createElement('span')
    span.className = 'cm-newline'
    span.textContent = '↵'
    return span
  }
}

function highlightNewLine() {
  return ViewPlugin.fromClass(
    class {
      decorations: DecorationSet
      constructor(view: EditorView) {
        this.decorations = this.getDecorations(view)
      }

      getDecorations(view: EditorView) {
        const widgets = []
        for (const { from, to } of view.visibleRanges) {
          for (let pos = from; pos <= to;) {
            const line = view.state.doc.lineAt(pos)
            if (line.length === 0) {
              widgets.push(Decoration.widget({ widget: new NewlineWidget(), side: 1 }).range(pos))
            }
            else {
              widgets.push(Decoration.widget({ widget: new NewlineWidget(), side: 1 }).range(line.to))
            }
            pos = line.to + 1
          }
        }
        return Decoration.set(widgets, true)
      }

      update(update: ViewUpdate) {
        if (update.docChanged || update.viewportChanged) {
          this.decorations = this.getDecorations(update.view)
        }
      }
    },
    {
      decorations: v => v.decorations,
    },
  )
}

export { highlightNewLine }

style

.cm-newline {
  color: currentColor;
  pointer-events: none;
  opacity: 0.5;
}
2 Likes

Thanks @hungify , this is just what I needed!