Skip to content

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
AppFlowy-IO:mainfrom
rodolfogoulart:aletheia-fix-moveVertical
Open

fix: vertical cursor navigation issues across multiline nodes, line boundaries, and nested child blocks in moveVertical#1232
rodolfogoulart wants to merge 2 commits into
AppFlowy-IO:mainfrom
rodolfogoulart:aletheia-fix-moveVertical

Conversation

@rodolfogoulart

Copy link
Copy Markdown

Fix vertical cursor navigation issues across multiline nodes, line boundaries, 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.

Gravacao.de.Tela.2026-08-12.185041.mp4

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.|").

Automated Tests

  • Ran flutter test test/extensions/ — all 42/42 tests passed cleanly.

…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.|"`).
@rodolfogoulart rodolfogoulart changed the title Fix vertical cursor navigation issues across multiline nodes, line boundaries, and nested child blocks in moveVertical fix: vertical cursor navigation issues across multiline nodes, line boundaries, and nested child blocks in moveVertical Aug 12, 2026
When on the end of the line and is on bullet blocks with many levels, was getting stuck on the last node.
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