如何为 CodeMirror Widget 添加 lint marker

CodeMirror6 中可以创建 Atomic Rranges 用自定义 Widget 替换文本,从而实现更加丰富的展示效果。但是当文本被 Widget 替换后,lint 功能的 markClass 并不会被添加到生成的 Widget 元素上,这就会导致用户无法在 Widget 上看到 linter 生成的错误标记。

CodeMirror 并没有提供直接地解决方案,我们需要自己将 lint 结果绑定到 Widget 上。由于 Widget 的渲染时机跟 linter 的运行时间都是黑盒,可以假设这俩都会在编辑器内容更新后被异步并发调用。为了方便将 linter 异步产生的结果同步给 Widget ,可以用 rxjs 的 BehaviorSubject 存储 linter 的结果。

import { Diagnostic } from '@codemirror/lint';
import { BehaviorSubject } from 'rxjs';

const diagnostics$ = new BehaviorSubject<Diagnostic[]>([]);

在实现 linter 时,除了直接返回 Diagnostic 外,还需要将结果写入 diagnostics$

const invalidMetric = linter(async view => {
  const diagnostics: Diagnostic[] = await yourLinter(view);
  diagnostics$.next(diagnostics);
  return diagnostics;
});

接下来,我们在创建 Widget 时就可以订阅 Diagnostic 的变化来更新 UI 状态:

class PlaceholderWidget extends WidgetType {
  constructor(string, private from: number, private to: number) {
    super();
  }

  toDOM(view: EditorView): HTMLElement {
    const span = document.createElement('span');
    span.innerHTML = renderYourWidget(view);
    // @ts-expect-error add destroy$ to span
    span.destory$ = diagnostics$.subscribe(diagnostics => {
      const error = diagnostics.find(d => d.from >= this.from && d.to <= this.to);
      if (error) {
        span.classList.add('cm-widget-error');
      } else {
        span.classList.remove('cm-widget-error');
      }
    });
    return span;
  }

  destroy(el: HTMLSpanElement) {
    // @ts-ignore
    if (el.destory$) {
      // @ts-ignore
      el.destory$.unsubscribe();
    }
  }
}

注意 PlaceholderWidget 的构造函数,我们需要在创建 Widget 的时候,传入 Widget 的插入位置(fromto)。

const placeholderMatcher = new MatchDecorator({
  regexp: /#[0-9A-Za-z_]+/g,
  decorate: (add, from, to, match) => {
    const deco = Decoration.replace({
      widget: new PlaceholderWidget(from, to),
    });
    add(from, to, deco);
  },
});
const placeholders = ViewPlugin.fromClass(class {
  placeholders: DecorationSet;

  constructor(view: EditorView) {
    this.placeholders = placeholderMatcher.createDeco(view);
  }

  update(update: ViewUpdate) {
    this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
  }
}, {
  decorations: instance => instance.placeholders,
  provide: plugin => EditorView.atomicRanges.of(view => {
    return view.plugin(plugin)?.placeholders || Decoration.none;
  }),
});

最终的效果如下:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注