How to achieve visible tabs in CodeMirror 6?

I found this example: CodeMirror: Visible tabs demo but I think it features CodeMirror 5. How to code it myself with CodeMirror 6?

I want to do “visible spaces”, and I think I can edit “visible tabs” to spaces.

There’s something like that here in the Chrome devtools source.

@marijn Okay, I managed to create this ViewPlugin based on their example:

const matchDecorator = new MatchDecorator({
  regexp: /^ +/gm,
  decoration: match => Decoration.mark({
    attributes: {class: 'cm-highlightedSpaces', 'data-display': '·'.repeat(match[0].length)},
  }),
  boundary: /\S/,
});

const showAllWhitespace = ViewPlugin.define(
  view => ({
    decorations: matchDecorator.createDeco(view),
    update(viewUpdate) {
      this.decorations = matchDecorator.updateDeco(viewUpdate, this.decorations);
    },
  }),
  {
    decorations: view => view.decorations,
  });

But I’m using markdown language, and I only want to run these decorations, when in CodeBlock element. Where should add the check?

@marijn I implemented the same thing as before, but without MatchDecorator, but by iterating of syntaxTree() myself:

import {Decoration, ViewPlugin} from "@codemirror/view"
import {syntaxTree} from "@codemirror/language"
import {RangeSetBuilder} from "@codemirror/state";

const cachedDecorations = new Map();

function whitespaceDecoration(spaces) {
  const cached = cachedDecorations.get(spaces);
  if (cached) {
    return cached;
  }
  const decoration = Decoration.mark({
    attributes: {class: 'cm-visibleSpaces', 'data-spaces': '·'.repeat(spaces.length)},
  });
  cachedDecorations.set(spaces, decoration);
  return decoration;
}

function decorations({view}) {
  const builder = new RangeSetBuilder();

  for (const {from, to} of view.visibleRanges) {
    syntaxTree(view.state).iterate({
      from, to,
      enter: syntaxNode => {
        if (syntaxNode.name === "FencedCode") {
          const content = view.state.doc.sliceString(syntaxNode.from, syntaxNode.to);

          const myRe = /^ +/gm;
          let myArray;

          while ((myArray = myRe.exec(content)) !== null) {
            const offset = myArray.index;
            const length = myArray[0].length;

            builder.add(
              syntaxNode.from + offset,
              syntaxNode.from + offset + length,
              whitespaceDecoration(myArray[0]));
          }
        }
      }
    })
  }
  return builder.finish();
}

export const visibleSpaces = ViewPlugin.define(view => ({view}), {decorations});

Which of these approaches would be better?

  1. The first approach uses MatchDecorator, isn’t that inefficient? I would image it probably does a global match.
  2. The second approach only iterates .visibleRanges, but has to parse the tree with syntaxTree().

Which would be better?