block / block/buzz

[Bug] Composer drops space after apostrophe word at start of line following a blank line

Open
#2,729 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

In the message composer, a **blank line followed by a first word containing an apostrophe** drops the space after that word. The missing space is present in the **stored message content**, not just the rendering — so this is a composer/input bug, not a display bug.

## Reproduction

1. Type a line, press Enter twice (leaving a blank line).
2. On the new line, start with a word containing an apostrophe, e.g. `It's better`.
3. The result is stored as `It'sbetter` — the space after the apostrophe word is eaten.

Observed variants (all reproduced in real message content):
- After 2+ hard breaks: `It's better` → `It'sbetter`
- After a single blank line: `Let's try` → `Let'stry`
- On a normal single-newline line (no preceding blank line): `Let's try` stays intact ✅

The trigger requires **both**: (a) at least one blank line / consecutive hard break immediately before, and (b) the first word after the break containing an apostrophe that the OS renders as a curly quote (U+2019, `'`).

## Root cause (high confidence)

- The composer is TipTap + `tiptap-markdown` configured with `breaks: true` — `desktop/src/features/messages/lib/useRichTextEditor.ts:485`. Enter submits, and blank lines become **consecutive hard breaks** (`

`) rather than paragraph nodes.
- The apostrophe in the failing cases is a macOS **smart-quote** substitution: the OS replaces the straight `'` with a curly `'` (U+2019) via a text-replacement / composition `input` event as you type.
- When that OS-level substitution lands on the **first word immediately after one or more hard breaks**, ProseMirror re-reads the mutated `contenteditable` and its DOM-diff drops the adjacent space at the `
`→text boundary. Single-newline lines don't sit on that boundary, which is why they survive — matching the observed variants above.

There is no smart-quote / U+2019 handling in `desktop/src/features/messages/lib/` today (confirmed by grep), so nothing intercepts this path.

## Why this wasn't caught in e2e testing

The composer e2e specs drive text input programmatically rather than through the OS input stack:

- `.fill()` sets the field value in bulk — e.g. `desktop/tests/e2e/mentions.spec.ts` (`input.fill("Ask @fi")`, etc.).
- `.pressSequentially()` emits synthetic per-character keystrokes — e.g. `desktop/tests/e2e/composer-link-shortcut.spec.ts:29` and `desktop/tests/e2e/custom-emoji-ui.spec.ts:30`.

Neither path emulates the macOS **smart-quote text-replacement / composition `input` event** that this bug depends on. The substitution (and the DOM mutation that follows) only happens through the real OS input path, so the failing content is never produced under Playwright. In short: our composer coverage exercises the happy typing path but not OS-level text substitution against hard-break boundaries.

## Notes on rigor

- The dropped space was confirmed in **stored** message content, not just rendering.
- A **headless** editor harness (jsdom + faithful `insertText`) preserves the space — the drop only manifests through the real `contenteditable` + OS smart-quote path, which a headless harness can't fully mimic. The mechanism above is the strongest supported explanation, not a line stepped through in a debugger.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.