Skip to content

maxLengthSingleLineRendering truncation can split grapheme clusters (emoji/ZWJ) #104

@XiaoBuHaly

Description

@XiaoBuHaly

Problem

When using CodeEditor.maxLengthSingleLineRendering, Re-Editor truncates a long single line by doing substring(0, N) on TextSpan.text / plainText. This counts UTF-16 code units and may cut in the middle of a grapheme cluster (surrogate pairs, ZWJ sequences, variation selectors).

This can lead to broken rendering (e.g. replacement character "�"), and potentially inconsistent caret/selection hit-testing near the truncation boundary.

Where

lib/src/_code_paragraph.dart:

// current implementation
if (renderingLength != null && plainText.length > renderingLength) {
  impl = _build(trucate(span, renderingLength), plainText.substring(0, renderingLength), true);
}

TextSpan trucate(TextSpan span, int maxLength) {
  // ...
  if (text.length > remainingLength) {
    text = text.substring(0, remainingLength);
  }
  // ...
}

Repro

  1. Enable wordWrap: true
  2. Set maxLengthSingleLineRendering to a small number
  3. Type/paste an emoji sequence around the boundary, e.g.:
    • Surrogate pair: 😀
    • ZWJ sequence: 👨‍👩‍👧‍👦 or 👩‍❤️‍💋‍👩
  4. If truncation cuts inside the sequence, rendering becomes broken.

Suggestion

Make truncation grapheme-safe:

  • Use package:characters (grapheme cluster iterator) to compute a safe prefix length <= maxLength (code units) and truncate at cluster boundaries.
  • Apply the same safe length to both the span truncation and the plainText substring.

This keeps the performance guard while avoiding invalid Unicode sequences.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions