fix: vertical cursor navigation issues across multiline nodes, line boundaries, and nested child blocks in moveVertical - #1232
Open
rodolfogoulart wants to merge 2 commits into
Conversation
…undaries, and nested child blocks in moveVertical
## Description
Fixes several vertical caret movement issues (`Up` and `Down` arrow navigation) in `PositionExtension.moveVertical` when dealing with multiline blocks, line-wrapped soft boundaries, cross-node transitions, and nested child block structures.
---
## Problem Statement & Bug Scenarios
### 1. **Cross-Node Upward Jump to First Line of Multiline Paragraphs**
* **Bug:** When moving `Up` from a single-line or multiline block into an adjacent multiline block located above it, if the caret was positioned near or at the end of the current line (where the horizontal coordinate $X$ exceeded the right edge of the target line above), Flutter's `RenderParagraph.getPositionForOffset` fell back to offset `0`. As a result, the caret jumped to the **first line** of the target block instead of landing on its **last visual line**.
* **Impact:** Unexpected cursor movement to the top of a paragraph when navigating upwards.
### 2. **Cross-Node Upward Navigation Skipping Nested Child Blocks**
* **Bug:** When moving `Up` from a block located beneath a parent block containing nested child nodes (e.g., bulleted lists inside a parent paragraph), fallback neighbor resolution (`nodePath.previous`) selected the parent node `[0]` instead of traversing down to its deepest last child node `[0, N]`.
* **Impact:** The cursor skipped all nested bullet list items and jumped straight to the parent paragraph header text above.
### 3. **Line Boundary Caret Projection on Soft-Wrapped Lines (Same-Node & Cross-Node)**
* **Bug:** On line-wrapped text, when caret horizontal coordinate $X$ exceeded the end of a line above/below, `getOffsetForCaret` returned an offset pointing to the soft-wrap boundary character at offset index 0 of the adjacent line, projecting the caret visually onto the wrong visual line.
* **Impact:** Caret jumped to the beginning of the current line or skipped intermediate lines during vertical arrow navigation.
### 4. **TextAffinity Downstream Defaulting on Paragraph End Offsets**
* **Bug:** When querying `getOffsetForCaret` at `maxOffset` (end of node text), Flutter defaults to `TextAffinity.downstream`, resolving the caret vertical coordinate `dy` to `0.0` (top line of paragraph) instead of the bottom line's `dy`.
* **Impact:** Cross-node target line calculations evaluated `lastLineDy` as `0.0`, routing the caret to line 1 of the target paragraph.
---
## Key Changes & Solution
### A. Symmetrical Cross-Node Line Constraint (`adjustCrossNodePosition`)
* **Upward Navigation (`upwards == true`):** Computes `lastLineDy` using `TextAffinity.upstream` at `maxOffset` to accurately pinpoint the bottom-most visual line of the target node. Iterates backwards through all offsets belonging to that last visual line to find the offset with the minimum horizontal distance `|caret.dx - localCaretX|`.
* **Downward Navigation (`upwards == false`):** Restricts caret placement strictly to characters belonging to the top-most visual line (`firstLineDy`) of the target node, ensuring the caret lands on line 1 of the next paragraph.
### B. Nested Child Node Resolution (`neighbourPath`)
* When resolving neighbor nodes during fallback cross-node upward navigation, if `upwards == true` and the target node contains children (`neighbour.children.isNotEmpty`), the algorithm recursively traverses to the last leaf child node (`neighbour.children.last`) before applying `adjustCrossNodePosition`.
### C. Soft-Wrap Boundary Offset Compensation
* When moving `Up` or `Down` on wrapped text, if `newCaretDy` crosses beyond the target line boundary height, the algorithm steps back 1 character index (`newPosition.offset - 1`) to ensure the caret visually stays at the end of the intended visual line.
---
## Test Cases & Verification
### Scenario 1: Cross-Node Upward Navigation into Multiline Block
* **Document:**
- Block 1 (7 lines): `"Davi, porém insistiu: ... 1 Samuel 17:34-36"`
- Block 2 (1 line): `"Então veja: Ser pastor de ovelhas, não era uma coisa fácil."`
* **Action:** Place caret at the end of Block 2 and press `Up`.
* **Expected Result:** Caret moves to the end of the last line of Block 1 (`"...1 Samuel 17:34-36|"`).
* **Before Fix:** Caret jumped to Line 1 of Block 1 (`"Davi, porém insistiu: |"`).
### Scenario 2: Cross-Node Upward Navigation into Parent with Nested Children
* **Document:**
- Node `[0]` (Parent Paragraph): `"Eu dei enfaze no 'se'..."`
- Node `[0, 0]` (Bullet 1): `"Se não estamos..."`
- Node `[0, 1]` (Bullet 2): `"Seguiremos aquele..."`
- Node `[0, 2]` (Bullet 3): `"Na verdade o ouviremos, mas não o reconheceremos..."`
- Node `[1]` (Paragraph): `"Como ovelhas do rebanho de Jesus..."`
* **Action:** Place caret at the end of Node `[1]` and press `Up`.
* **Expected Result:** Caret moves to the end of Node `[0, 2]` (`"...não teremos escolha.|"`).
* **Before Fix:** Caret jumped past all bullets to Node `[0]` (`"...sequestrado pelo inimigo.|"`).
moveVerticalmoveVertical
When on the end of the line and is on bullet blocks with many levels, was getting stuck on the last node.
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.
Fix vertical cursor navigation issues across multiline nodes, line boundaries, and nested child blocks in
moveVerticalDescription
Fixes several vertical caret movement issues (
UpandDownarrow navigation) inPositionExtension.moveVerticalwhen dealing with multiline blocks, line-wrapped soft boundaries, cross-node transitions, and nested child block structures.Gravacao.de.Tela.2026-08-12.185041.mp4
Problem Statement & Bug Scenarios
1. Cross-Node Upward Jump to First Line of Multiline Paragraphs
Upfrom a single-line or multiline block into an adjacent multiline block located above it, if the caret was positioned near or at the end of the current line (where the horizontal coordinateRenderParagraph.getPositionForOffsetfell back to offset0. As a result, the caret jumped to the first line of the target block instead of landing on its last visual line.2. Cross-Node Upward Navigation Skipping Nested Child Blocks
Upfrom a block located beneath a parent block containing nested child nodes (e.g., bulleted lists inside a parent paragraph), fallback neighbor resolution (nodePath.previous) selected the parent node[0]instead of traversing down to its deepest last child node[0, N].3. Line Boundary Caret Projection on Soft-Wrapped Lines (Same-Node & Cross-Node)
getOffsetForCaretreturned an offset pointing to the soft-wrap boundary character at offset index 0 of the adjacent line, projecting the caret visually onto the wrong visual line.4. TextAffinity Downstream Defaulting on Paragraph End Offsets
getOffsetForCaretatmaxOffset(end of node text), Flutter defaults toTextAffinity.downstream, resolving the caret vertical coordinatedyto0.0(top line of paragraph) instead of the bottom line'sdy.lastLineDyas0.0, routing the caret to line 1 of the target paragraph.Key Changes & Solution
A. Symmetrical Cross-Node Line Constraint (
adjustCrossNodePosition)upwards == true): ComputeslastLineDyusingTextAffinity.upstreamatmaxOffsetto accurately pinpoint the bottom-most visual line of the target node. Iterates backwards through all offsets belonging to that last visual line to find the offset with the minimum horizontal distance|caret.dx - localCaretX|.upwards == false): Restricts caret placement strictly to characters belonging to the top-most visual line (firstLineDy) of the target node, ensuring the caret lands on line 1 of the next paragraph.B. Nested Child Node Resolution (
neighbourPath)upwards == trueand the target node contains children (neighbour.children.isNotEmpty), the algorithm recursively traverses to the last leaf child node (neighbour.children.last) before applyingadjustCrossNodePosition.C. Soft-Wrap Boundary Offset Compensation
UporDownon wrapped text, ifnewCaretDycrosses beyond the target line boundary height, the algorithm steps back 1 character index (newPosition.offset - 1) to ensure the caret visually stays at the end of the intended visual line.Test Cases & Verification
Scenario 1: Cross-Node Upward Navigation into Multiline Block
"Davi, porém insistiu: ... 1 Samuel 17:34-36""Então veja: Ser pastor de ovelhas, não era uma coisa fácil."Up."...1 Samuel 17:34-36|")."Davi, porém insistiu: |").Scenario 2: Cross-Node Upward Navigation into Parent with Nested Children
[0](Parent Paragraph):"Eu dei enfaze no 'se'..."[0, 0](Bullet 1):"Se não estamos..."[0, 1](Bullet 2):"Seguiremos aquele..."[0, 2](Bullet 3):"Na verdade o ouviremos, mas não o reconheceremos..."[1](Paragraph):"Como ovelhas do rebanho de Jesus..."[1]and pressUp.[0, 2]("...não teremos escolha.|").[0]("...sequestrado pelo inimigo.|").Automated Tests
flutter test test/extensions/— all 42/42 tests passed cleanly.