I created an extension to collapse and display long text, but I'm encountering issues when pasting long text in places where there are characters before or after it.

textShortener

I’ve provided an example here. The extension works correctly; however, when pasting long text in a location that has characters before or after it, although the long text is collapsed as expected, a long blank space appears after it.

If I use EditorView.lineWrapping, this blank space appears below the text instead, but the threshold for triggering the issue becomes higher.

Additionally, pressing Enter after patterns like [](long text...) also results in a long blank space.

I’m reaching out for help here, as I’m not sure whether this is an issue with the editor itself or if my extension is not configured correctly.

Decorations created by view plugins should not substantially change the geometric structure/size of the document, because they are only available after the editor has computed its layout. In this case, the code that looks for overlong lines to partially render doesn’t see the decoration, and thus assumes it needs to hide code that isn’t actually rendered.

For this extension, it should be quite easy to move to a state field, which solves the problem. I’m putting my port of your code below, which I used to test whether that worked.

import { RangeSetBuilder, StateField, Line, EditorState } from '@codemirror/state'
import { Decoration, WidgetType, DecorationSet } from '@codemirror/view'

const defaultUrlRegex = /data:image\/png;base64,[a-z0-9+/=%]+/gi;

const shortenText = (text: string) => {
  return text.length > 10 ? text.slice(0, 10) + '...' : text;
};

class ShortTextWidget extends WidgetType {
  short: string;
  constructor(short: string) {
    super();
    this.short = short;
  }
  toDOM() {
    const span = document.createElement('span');
    span.textContent = this.short;
    span.className = 'cm-short-text';
    span.style.display = 'inline';
    return span;
  }
  eq(other: ShortTextWidget) {
    return this.short === other.short;
  }
}

function shorten(state: EditorState) {
  const builder = new RangeSetBuilder<Decoration>();
  for (let i = 1; i <= state.doc.lines; i++) {
    const line = state.doc.line(i);
    scanLine(line, builder);
  }
  return builder.finish();
}

export const textShortener = StateField.define<DecorationSet>({
  create(state) {
    return shorten(state)
  },

  update(deco, tr) {
    return tr.docChanged ? shorten(tr.state) : deco
  },

  provide: f => EditorView.decorations.from(f)
})

function scanLine(line: Line, builder: RangeSetBuilder<Decoration>) {
  const text = line.text;

  let m;
  while ((m = defaultUrlRegex.exec(text))) {
    if (!m[0]) continue

    const raw = m[0];
    if (raw.length > 100) {
      const short = shortenText(raw);
      builder.add(
        line.from + m.index,
        line.from + m.index + raw.length,
        Decoration.replace({ widget: new ShortTextWidget(short) })
      );
    }
  }
}
1 Like

It works :+1:. Thank you so much

Do note that that implementation (as well as your original one) will scan the entire document on every change, which may get expensive at some size.