Skip to content

Keep the selection on a Shift-click past the end of a line - #284

Open
null00x wants to merge 1 commit into
TelegramMessenger:masterfrom
null00x:fix/shift-click-past-line-end
Open

null00x wants to merge 1 commit into
TelegramMessenger:masterfrom
null00x:fix/shift-click-past-line-end

Conversation

@null00x

@null00x null00x commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Key change

Shift-clicking to the right of the end of a line in a message no longer moves the start of the text selection to −1. The click now leaves the selection as it was. Before, the highlight spread back over the earlier lines, and Quote put a stray character and the text before the selection into the reply.

Reason

Message text that the layer renderer can't draw, for example a message with a spoiler (ChatLayerMessageText+Layout.swift), is drawn by TextView. A Shift-click in TextView.mouseUp extends the selection to the character from findCharacterIndex(at:). That returns −1 when the click is to the right of the line's end, and such a click is still inside the text view when another line of the message is longer. The Shift branch didn't check for −1. Since −1 is less than the selection's start, it set range.location = -1 and lengthened the range.

The highlight then covered everything from the start of the text. FoldingTextLayout.merged passes the range on to the message menu, which reads it while it is built. The Quote item checks for NSNotFound and NSMaxRange but not for a negative location, so it called attributedSubstring(from:) with location −1. The merged string is an NSMutableAttributedString, and it doesn't throw for that range: it returns one UTF-16 unit from just before its characters, then the rest. In the test below that unit was 0x1C, the length of the 28-character message, so the quote became "\u{1C}Hi\n" plus the selected word, with offset −1. In a later block of a message the range starts on the last character of the block before instead. Copy Selected Text, Copy, Translate and copying a link from the selection read the same range, and media captions build the same menu from merged.

The Shift branch now ignores an index below 0. The layer-drawn message text already does this in its own Shift branch (ChatLayerTextNode.swift), and selectWord(at:) returns early on −1. Shift-clicks on characters work as before. The change is in TGUIKit's TextView, so it applies to every selectable text view, but only to clicks where the index is −1.

iOS has no Shift-click. Its Quote action clamps the selected range to the text (normalizedSelectionRange in ChatMessageTextBubbleContentNode.swift).

Screenshot/video result (before/after)

Before

The word on the second line was double-clicked, then Shift-clicked to the right of Hi. The highlight also covers Hi, and Quote puts Hi and the word into the reply panel. The panel's text starts with a 0x1C character, which isn't visible.

After

The same steps. The selection stays on the word, and Quote quotes only the word.

Confirmation

Tested on macOS 26.6.2 (25G83) in a Debug build (13.1.0, 283357), signed in to a test environment account. Both builds were master e1883098 with a local test harness and unrelated local changes. Those include edits in TextView.swift (copy-protection checks in the block-quote header click and in copy(_:)) and in ChatMessageMenuItems.swift, none of them in the Shift-click branch or the Quote item. The After build also has this change.

Steps:

  1. In a group, send a two-line message: Hi, then a word and a spoiler (quote<number> hidden, with hidden as a spoiler).
  2. Double-click the word on the second line.
  3. Shift-click the empty space to the right of Hi, inside the bubble.
  4. Right-click the message and choose Quote.
Case Expected Before After
Shift-click to the right of Hi The selection stays on the word The highlight also covers Hi Only the word is highlighted
Quote The reply panel quotes the word The panel shows Hiquote…; its text is 0x1C, Hi, a line break and the word The panel shows quote…

These ran through a scripted test in a test group, one pass per build. The test reads the reply panel's text through accessibility.

tools/tests/shift-click-past-line-end takes findIndex(location:), findCharacterIndex(at:), the Shift branch of mouseUp, the Shift branch of ChatLayerTextNode, FoldingTextLayout.merged, the Quote item's check and substring, the Copy Selected Text handler and TextView.copy(_:) from the source. It lays out real CoreText lines and runs 13 cases: Shift-clicks past the end of a short first or last line, with the selection before or after that line or at the start of the text, in the first or second block of a message; Shift-clicks on characters before, inside and after the selection; and three clicks past the end of a line that must not enter the Shift branch, with no selection, as a double-click and without Shift. Each case runs in a separate process, so a crash would show up as a failure. The test makes 100 checks. On master 42 of them fail, all in the 6 cases that Shift-click past the end of a line with a selection: the selection starts at −1, and Quote, Copy Selected Text and Copy get a stray character and the text before the selection (in a message's second block, Quote and Copy Selected Text get the first block's last character instead). Nothing crashes there. All 9 mutants of the fix that change its behavior are caught, including a guard at the Quote item alone.

Limitations:

  • One pass per build in the app, with one message in one group. Other line positions, Copy Selected Text and Copy are covered by the test only.

  • The scripted test doesn't send the quoted reply, so what the server does with a quote at offset −1 isn't checked.

  • The app builds were based on e1883098, not the current master. TextView.swift differs between them only in rightMouseDown (public → open) and the block-quote header click. ChatMessageMenuItems.swift differs too, but not in the Quote item or the Copy Selected Text, Copy Link and Translate items. The test runs against the current master source.

  • Translate, copying a link and media captions read the same range, but weren't tested separately.

  • I confirm that I have tested the affected behavior and verified that it works properly, or verified the content and links for a documentation-only change.

Message text that can't use the layer renderer, such as a message
with a spoiler, is drawn by TextView. A Shift-click there extends the
selection to findCharacterIndex(at:), which returns -1 for a click to
the right of a line's end. Such a click lands inside the text view
when another line of the message is longer. The Shift branch of
mouseUp didn't check for -1, so it moved the selection's start to -1.

The highlight then reached back over the start of the text, and the
Quote item read the range from -1. In the first block of a message
the quote was a stray character read from before the string's
characters, the text before the selection and the selected text,
with offset -1. Copy Selected Text, Copy, Translate and copying a
link read the same range.

The Shift branch now ignores an index below 0, as the Shift branch of
ChatLayerTextNode and selectWord(at:) already do.
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.

1 participant