Skip to content

fix(editor): code block Backspace/Delete after rich paste (#2196)#2199

Open
Toseef-Ahmad wants to merge 1 commit into
anyproto:developfrom
Toseef-Ahmad:fix/2196-code-block-backspace-invisible-unicode
Open

fix(editor): code block Backspace/Delete after rich paste (#2196)#2199
Toseef-Ahmad wants to merge 1 commit into
anyproto:developfrom
Toseef-Ahmad:fix/2196-code-block-backspace-invisible-unicode

Conversation

@Toseef-Ahmad

@Toseef-Ahmad Toseef-Ahmad commented May 2, 2026

Copy link
Copy Markdown
Contributor

You can paste this into the PR description on GitHub:

Summary
Fixes #2196 — after pasting content from rich sources (e.g. AI “Copy response”, Markdown-heavy snippets) into a code block, Backspace and Delete often did not remove characters, or editing felt “stuck” even though new typing still worked.

What was going wrong
Code blocks render as a contenteditable whose HTML is built from plain text (syntax highlighting with Prism, newlines as
). That stack is different from normal paragraph text with marks.

Several things could line up badly:

Invisible Unicode (zero-width spaces, BOM, word joiner, etc.) from the clipboard could end up in the stored text. That made Mark.hasZws() treat the field like mark-anchored rich text and turned on ZWS-aware DOM ↔ model range conversion — which is meant for mentions/emoji, not for source code. Caret and offsets could disagree with what you see.

Backspace handling used if (range.to) as a proxy for “not at the start of the block”. In JavaScript, 0 is falsy, so a collapsed caret at offset 0 was mixed with broken or missing range.to. That could run the wrong branch, re-save stale text, set ret = true, and skip the rest of the key handler — fighting the browser’s native delete.

Even after tightening ranges and ZWS behavior, native contenteditable deletion inside long pasted
/ Prism trees could still disagree with the plain-text model the app persists. The reliable fix for code is to not depend on the browser for single-character Backspace/Delete there.

What we changed

  1. Normalize pasted / stored code text
    Added U.String.stripZeroWidthFormatChars() and call it when reading/writing code block text so invisible format characters do not accumulate in the model.
  2. Correct selection mapping for code fields
    Editable: new prop plainDomTextOffsets — for code blocks, selection offsets match selection-ranges directly (no Mark ZWS dom/model conversion).
    focus.apply: if the focused node is inside .textCode, skip modelToDom ZWS conversion.
    BlockText onFocus: skip domToModel for code when reading the selection from the DOM.
  3. Safer Backspace conditions (all text, especially edge cases)
    Replaced fragile if (range.to) / !range.to logic with explicit checks: non-empty caret or selection vs collapsed at offset 0.
    onBackspaceBlock: merge-at-start-of-block now requires range.from === range.to === 0 instead of !range.to, so missing to does not look like “start of block”.
  4. Programmatic Backspace / Delete inside code blocks
    For code blocks only (menus closed): on Backspace / Delete, preventDefault and update the block using the current getTextValue() string:
    Selection: remove the selected range.
    Collapsed caret: remove one UTF-16 code unit or a full surrogate pair (emoji) before/after the caret via U.String.utf16DeleteLengthBefore / utf16DeleteLengthAfter.
    Then blockSetText + setValue so the DOM and store stay aligned.
    Caret at offset 0 is not intercepted here so merge with the previous block still works as before.
    Scope / risk
    Touched files (5): string.ts, block/text.tsx, form/editable.tsx, lib/focus.ts, editor/page.tsx.
    Behavior change is scoped to code-style text blocks and Backspace/Delete (plus the small, targeted range and focus fixes above).
    Non-code paragraphs, titles, lists, etc. keep their existing paths; mark-related Backspace logic stays in the non-code branch.
    How to verify (manual)
    Copy a long snippet from an AI assistant or any source that puts Markdown + emoji + fenced blocks in the clipboard.
    Paste into a code block in the editor.
    Place the caret inside the pasted text and use Backspace and Delete — each key should remove one visible character (or the selection), including around emoji.
    Move the caret to the very start of the block and press Backspace — block merge / previous-block focus should still behave as today.
    Smoke-test a normal paragraph (not code): Backspace, Delete, marks, and merge-at-start unchanged.
    Checklist

Fixes #2196 (code block editing after rich paste)

Single logical change-set; no unrelated refactors

Typecheck / lint clean on changed files

Short title for the PR:
fix(editor): code block Backspace/Delete after rich paste (#2196)

I have read the CLA Document and I hereby sign the CLA

)

Rich clipboard / AI paste into code blocks broke character deletion when
DOM (Prism + <br>) and model offsets diverged.

- Strip zero-width/format chars in code setValue/getTextValue (U.String)
- Editable: plainDomTextOffsets for code; focus.apply skips ZWS mapping on
  .textCode; onFocus skips domToModel for code
- Tighten Backspace range checks; onBackspaceBlock uses explicit caret-at-0
- Code blocks: handle Backspace/Delete programmatically on the model string
  (UTF-16 surrogate aware) with preventDefault to bypass unreliable native
  contenteditable deletion

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@ra3orblade

Copy link
Copy Markdown
Contributor

I will look at it more precisely next week, have a lot of stuff going in right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞 Bug Report: Backspace not working on pasted AI text inside code block

2 participants