{{ message }}
Repository navigation
fix(muya): select the clicked line ourselves on a triple-click (#5389) - #5524
Merged
Merged
Conversation
Jocs
force-pushed
the
fix/5389-triple-click-line-selection
branch
2 times, most recently
from
September 23, 2026 04:07
6bf7a6f to
198add9
Compare
`getIndentSpace` in `codeBlockContent` spelled out `lastIndexOf('\n', offset - 1)
+ 1` / `indexOf('\n', start)` with its own `-1 → length` fallback, and the
triple-click work needs the same pair. Move it to `utils` as `lineBounds` so the
off-by-one lives in one tested place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`findContentDOM` → read the stamped block → `outMostBlock` gate → `getOffsetOfParagraph(node, dom) + offset` is the answer to "which block and character does this DOM position mean", and `getSelection()` was the only place that knew it — inline, for both ends at once. Lift it into `selection/dom.ts` as `resolveEndpoint` so a caller that has one DOM position rather than a whole range can ask the same question. The cast the callsite used to make is now `getBlock()`'s job. Behaviour is unchanged: every bail-out the helper folds in already made `getSelection()` return null for the whole selection, and the region it replaces was side-effect free. Also names the anchor/focus pair `ISelectionEndpoints`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A triple-click highlighted one line, but typing a character consumed the block below it: a thematic break disappeared, a list's first item or a quote's text was pulled into the line, front matter absorbed the body paragraph and lost its own. Above a table or a task list the screen changed and the document did not; above a heading nothing was highlighted at all and the character landed at the start of the line. Chromium's select-paragraph range reaches past what it highlights, and where it reaches depends on what follows the clicked line: offset 0 of the next block; a node that maps to no content block (a table, a task-list checkbox, a math or html preview); the character after the line's own `\n` inside a code block or front matter; or, above a heading and on a code block's last line, nowhere at all. muya stored that range as the user's own, so `Clipboard.keydownHandler` ran `cutSelection`, whose `resolveCutEnd` turns an end offset of 0 into "merge the whole end block", and where the range resolved to no block the keystroke neither cut nor called `preventDefault()` — leaving the browser to edit the DOM with the document behind it (#5414, one of #5035's triggers). Don't consume that range: cancel it. The third mousedown of a click streak is default-prevented, the line under the pointer is found from the pointer's own coordinates, and muya sets the selection itself. There is no engine range to correct, so the collapsed cases, the unresolvable ones and the `\n` overshoot inside a multi-line block all stop existing rather than being repaired, and all three engines behave the same (Firefox and WebKit select only the clicked word on their own). The cost is that a triple-click no longer extends as the pointer drags on — cancelling the mousedown leaves the browser's drag-select unarmed. The selection stays the clicked line, which the e2e pins in all three drag directions. Fixes #5389 Fixes #5414 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jocs
force-pushed
the
fix/5389-triple-click-line-selection
branch
from
September 23, 2026 11:26
08c0949 to
8813d26
Compare
`resolveEndpoint` stopped one field short of what both its callers wanted: each rebuilt an `IAnchorFocusInfo` by re-adding `block.path`, so `getSelection()` carried two locals and two literals for it and `paragraphSelect` needed a factory of its own. Return the endpoint itself. With that, `paragraphSelect.ts` was one exported function whose only dependency and only caller both lived in `selection/`, so fold it in as `lineAtPoint` beside `resolveEndpoint` — the point-to-caret half is a general capability the next point-based feature will want, not something about paragraphs. The caller then says what it means with the API that already exists for it, `Content#setCursor(start, end)`, instead of assembling both endpoints by hand, and clears `isSelect`, which is where "this gesture will not drag" belongs. `ISelection` now extends `ISelectionEndpoints` rather than redeclaring the pair three lines below it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A review raised that `detail >= 3` also matches a shift-held click streak, and suggested excluding it so the shift-extend path keeps the selection. Measuring it says otherwise: Chromium's third shift-click discards the extension and select-paragraphs at the click, overshoot and all — so excluding it re-opens #5389 for that gesture (`alpha beta` above `***` saves as `Z***`). The predicate is right as it stands; what was missing is a test saying so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Triple-clicking a line highlights only that line, but typing a character also consumed the block below it: a thematic break disappeared, a list's first item or a quote's text was pulled into the line, front matter absorbed the body paragraph and lost its own. Above a table or a task list the screen changed but the document did not, and above a heading the triple-click highlighted nothing at all and the character landed at the start of the line.
All nine variants in #5389 now behave as that issue's "Expected result" column specifies, verified in the packaged app with the file re-read from disk.
Root cause
muya consumed the browser's select-paragraph range as if the user had made it. Captured from a real Chromium, that range reaches past what it highlights, and where it reaches depends on what follows the clicked line:
mu-contentspan<figure>/<li>/ preview node — nomu-contentancestor\nTextSelection.getSelection()calls a selection cross-block on block identity alone, so the first row became a cross-block selection.Clipboard.keydownHandlerthen rancutSelection, andresolveCutEndwith an end offset of 0 returns the end block's entire text as the tail to merge into the start block before removing it — so a block the user selected no character of was absorbed whole. In the second rowfindContentDOM(upward-only) resolved nothing,getSelection()returnednull, and the keystroke neither cut nor calledpreventDefault(), leaving the browser to edit the DOM with the document behind it (#5414, one of the #5035insertBeforetriggers). The third row explains the heading case, and the fourth joined two lines of one code block.The same selection fed Ctrl+X, paste (
applyPastedelegates tocutHandler) and Enter, and because the line read as cross-block,Editor._dispatchEventsnulledactiveContentBlock— which is why Cmd+B and the inline format toolbar did nothing over a triple-clicked line in a multi-block document. The existing triple-click specs use single-paragraph documents, where the range has no block to spill into, so none of this was covered.Fix
Don't consume that range — cancel it. The third mousedown of a click streak is default-prevented, the clicked line is located from the pointer's own coordinates (
caretPositionFromPoint, falling back tocaretRangeFromPoint), and muya sets the selection itself:Both guards are measured, not defensive: a right-click streak really does arrive with
detail = 3, button = 2, and excluding a shift-held streak would re-open the bug — Chromium's third shift-click discards the extension and select-paragraphs at the click, overshoot included. Eleven lines, and that is the whole behavioural change —handleMouseupOrLeaveandhandleMousemoveOrClickare untouched. Because no engine range is left to correct, the four shapes in the table above stop existing rather than being repaired: the collapsed cases, the unresolvable ones and the\novershoot inside a multi-line block are all covered by one rule, a triple-click selects the line under the pointer. All three engines then agree, where before Chromium selected a paragraph-and-a-bit while Firefox and WebKit selected the clicked word.Two supporting extractions, each its own commit:
lineBounds(shared withcodeBlockContent'sgetIndentSpace, which had the same off-by-one spelled out by hand) andresolveEndpoint(lifted out ofgetSelection(), which was the only place that knew how to turn one DOM position into a block and a character offset).lineAtPointsits beside it inselection/dom.ts, and the selection is installed withContent#setCursor, the API that already existed for exactly this.The cost: a triple-click no longer extends as the pointer drags on. Cancelling the mousedown leaves the browser's drag-select unarmed — measured: mid-drag the DOM selection stays exactly what we set and the browser never touches it again. The selection therefore stays the clicked line, which the e2e pins in all three drag directions rather than leaving it to chance. Re-arming that gesture would mean owning drag-extension and edge auto-scroll, which is not worth it for this one gesture; nothing is corrupted by its absence.
Two consequences worth calling out:
*overalpha betagives*alpha beta*) rather than replacing it — the same thing a drag-selected line has always done.\n\ngetSelectedText()used to append.Test plan
e2e/tests/editing/triple-click-line-5389.spec.ts(Chromium, 22 cases) — the real gesture over twelve following block types, plus a front-matter line, one line of a multi-line code block, a list item, a quoted line, Backspace, a shift-held click streak, [Bug] Typing over a triple-clicked line above a task list or table detaches it, and Enter on a list then throws "Failed to execute 'insertBefore'" #5414's own reproduction through to the Enter that used to throwinsertBefore, and the three drag directions. AssertsgetMarkdown()and an emptypageerrorlog.src/selection/__tests__/lineAtPoint5389.spec.ts+src/utils/__tests__/lineBounds.spec.ts(19 cases) —lineBoundsacross single-line, multi-line and empty text;resolveEndpointresolving a text node and returning null for a table, a task-list item and a detached node;lineAtPointbounding the first, middle and last line of a code block, and declining when the caret lands outside any content block. happy-dom has no layout, so the caret position is handed in; the gesture itself is the e2e's job.pnpm -C packages/muya test(2032 tests),lint,lint:types,check-circular.diagrams/plantuml, which reaches plantuml.com;edges/empty-and-tiny;inline/format-toolbar;inline/shortcuts); run in isolation the failing set is byte-identical with and without this branch, and identical again ondevelop.pnpm run build:unpack): issue [Bug] Typing over a triple-clicked line swallows the next block (thematic break, list item, quote, front matter) #5389's steps 1–5 for variants A–H plus a code-block line, driven through the real MarkText window, each asserting the file on disk after File > Save —xplus the untouched block, for all nine. A selection-heavy desktop e2e subset (editor input, inline format, selection word count, tab-switch cursor, search prefill, TOC active heading, paragraph blocks — 47 tests) passes against the refactoredgetSelection().Two follow-ups this deliberately leaves alone, both pre-existing and both verified in a real Chromium while reviewing this:
detail === 2— zero-width highlight, and one keystroke eats the block below (alpha beta+***→alpha betaZ***). The same cancel-and-select approach fits, but its no-drag half needs word bounds rather than line bounds.Clipboard.keydownHandler. That belongs to [Bug] Cmd+Backspace across two blocks detaches the second block, and Enter on a list then throws "Failed to execute 'insertBefore'" #5415.Fixes #5389
Fixes #5414
🤖 Generated with Claude Code