Visuele stand: Overzicht-markering flipt per toetsaanslag — twee concurrerende caret-berekeningen #2141

Closed
opened 2026-09-20 21:39:03 +00:00 by brenno · 0 comments
Owner

Symptoom

In de visuele stand flipt de Overzicht-markering tijdens het typen heen en weer tussen de juiste kop en een andere kop, en de app voelt traag. Gemeld als "na een tabel bewerkt en verlaten", maar het tabelbezoek is niet de trigger — het flippen speelt zodra de twee caret→kop-berekeningen het structureel oneens zijn (zie hieronder).

Oorzaak — twee concurrerende berekeningen van dezelfde markering, elk met eigen frame

Per Quill-melding (_onQuillChanged, markdown_editor.dart:557-580) loopt de outline-sync langs drie paden die elk hun eigen post-frame setState plannen via _setActiveOutlineIndex (document_editor_screen.dart:275):

  1. onVisualCaret → _syncOutlineToVisualCaret (document_editor_layouts.dart:46-64) schrijft eerst _controller.selection — dat vuurt _onControllerChanged → _syncOutlineToMarkdownCaret → index A via activeOutlineIndexForOffset (bron-offset).
  2. Daarna zet _syncOutlineToVisualCaret zelf index B via activeOutlineIndexInPlainText — een andere methode: indexOf op de koptitel in de platte tekst (markdown_outline.dart:78).
  3. De write-back (widget.controller.value = …, markdown_editor.dart:578) vuurt opnieuw _onControllerChanged → index C, weer via bron-offset.

Quill/IME meldt documentwijzigingen en selectiewijzigingen als aparte notificaties in verschillende frames. De laatste schrijver per frame wint — en dat wisselt → zichtbaar flippen zodra A/C ≠ B.

Wanneer ze het oneens zijn (beide methoden falen elk op hun eigen manier):

  • activeOutlineIndexInPlainText zoekt de letterlijke titelstring. Een kop met opmaak (## Paragraaf **vet**) staat in platte tekst als Paragraaf vet → indexOf mist hem → B rapporteert de kop dáárvoor.

  • activeOutlineIndexForOffset vertrouwt op MarkdownCaretMap.sourceOffsetOf, maar de kaart modelleert lang niet alle embeds die de codec kent. In Quill zijn dit elk één \uFFFC-teken; de kaart telt ze als volledige tekstregels:

    • ```mermaid-blokken: de kaart telt elke regel in de fence als zichtbare tekst (_fencePattern → _Row.text), Quill maakt er één EmbeddableMermaid-embed van → per diagram tientallen tekens verschil.
    • <!-- toc -->, <!-- timeline -->, <!-- pentest … -->-markers en voetnootdefinities [^1]: … → embeds, kaart telt ze als tekst.
    • inline [^label]-verwijzingen en inline ![alt](src)-afbeeldingen → 1 teken in Quill, meerdere in de kaart (stripInlineMarkdown houdt [^1] letterlijk, footnotes:false).

    Elke zo'n constructie boven de cursor laat sourceOffsetOf systematisch te vroeg landen → A/C wijzen de verkeerde kop aan, terwijl B wel goed zit — of andersom.

Waarom traag: per toetsaanslag lopen toPlainText, MarkdownCaretMap.of, toDelta, markdownFromDocument, tot 3× buildMarkdownOutline, een provider-edit én tot 3 losse post-frame setStates op de scherm-state — en _documentOutlineRail roept bij élke rebuild zelf weer buildMarkdownOutline(source) aan (document_editor_toolbar.dart:819). Alles O(document), per aanslag, meerdere keren.

Reproductie (widget-test, bewezen)

// Document met een mermaid-blok tussen twee koppen; caret in de paragraaf
// eronder; per aanslag de actieve rail-kop uitlezen:
const doc = '# Intro\n\n'
    '```mermaid\ngraph TD; A-->B; B-->C; C-->D;\n```\n\n'
    '| A | B |\n| --- | --- |\n| x | 1 |\n\n'
    '## Paragraaf\n\neerste regel\n\n## Slot\n\neinde\n';

// Metingen (typen van "regel toevoegen" + losse selectiemeldingen):
//   mermaid:   [Intro, Paragraaf*, Intro, Paragraaf*, Intro, Paragraaf*, …]
//   opmaakkop: [Intro, Paragraaf **vet***, Intro, Intro*, …]
//            (* = na selectie-only melding)

Zonder zo'n divergentie blijft de markering stabiel ([Paragraaf, Paragraaf, …]) — de controle slaagt.

Oplossingsrichting

  1. Eén berekening, één schrijver. Kies één pad voor de actieve kop in de visuele stand en laat alleen dat pad _setActiveOutlineIndex aanroepen. De platte-tekstvariant (B) volgt de werkelijke Quill-caret zonder kwetsbare offset-vertaling — maar moet dan wel om koptitels met opmaak heen kunnen (zoek op stripInlineMarkdown(title), of werk met blok-indexen i.p.v. indexOf op de titelstring). De bron-offsetvariant (A/C) is betrouwbaar zodra MarkdownCaretMap alle embed-types als object markeert — dat is dan de andere helft van de klus. Beide tegelijk laten schrijven is hoe dan ook de bug.
  2. MarkdownCaretMap bijwerken voor de embed-types: mermaid-fences, <!-- toc/timeline/pentest -->-markers, voetnootdefinities en inline voetnoot/afbeelding-embeds als 1-teken objecten meetellen, zodat bron-offset↔visueel-offset weer klopt in documenten die ze bevatten.
  3. Kosten terugdringen: buildMarkdownOutline niet 3–4× per aanslag draaien — één keer per bronwijziging cachen (of op de MarkdownSourceDocument/provider laten meerekenen) en de rail die versie laten gebruiken; en overwegen de outline-sync te dedupen per frame (max. één berekening per frame i.p.v. per notificatie).
  4. Regressietest: een document met een mermaid-blok of opmaakkop boven de cursor; na typen mag activeOutlineIndex niet alterneren — één stabiele kop per frame.
## Symptoom In de visuele stand flipt de Overzicht-markering tijdens het typen heen en weer tussen de juiste kop en een andere kop, en de app voelt traag. Gemeld als "na een tabel bewerkt en verlaten", maar het tabelbezoek is niet de trigger — het flippen speelt zodra de twee caret→kop-berekeningen het structureel oneens zijn (zie hieronder). ## Oorzaak — twee concurrerende berekeningen van dezelfde markering, elk met eigen frame Per Quill-melding (`_onQuillChanged`, `markdown_editor.dart:557-580`) loopt de outline-sync langs **drie** paden die elk hun eigen post-frame `setState` plannen via `_setActiveOutlineIndex` (`document_editor_screen.dart:275`): 1. `onVisualCaret` → `_syncOutlineToVisualCaret` (`document_editor_layouts.dart:46-64`) schrijft eerst `_controller.selection` — dat vuurt `_onControllerChanged` → `_syncOutlineToMarkdownCaret` → index **A** via `activeOutlineIndexForOffset` (bron-offset). 2. Daarna zet `_syncOutlineToVisualCaret` zelf index **B** via `activeOutlineIndexInPlainText` — een *andere* methode: `indexOf` op de koptitel in de platte tekst (`markdown_outline.dart:78`). 3. De write-back (`widget.controller.value = …`, `markdown_editor.dart:578`) vuurt opnieuw `_onControllerChanged` → index **C**, weer via bron-offset. Quill/IME meldt documentwijzigingen en selectiewijzigingen als aparte notificaties in verschillende frames. De laatste schrijver per frame wint — en dat wisselt → zichtbaar flippen zodra A/C ≠ B. **Wanneer ze het oneens zijn** (beide methoden falen elk op hun eigen manier): - `activeOutlineIndexInPlainText` zoekt de letterlijke titelstring. Een kop met opmaak (`## Paragraaf **vet**`) staat in platte tekst als `Paragraaf vet` → `indexOf` mist hem → B rapporteert de kop dáárvoor. - `activeOutlineIndexForOffset` vertrouwt op `MarkdownCaretMap.sourceOffsetOf`, maar de kaart modelleert lang niet alle embeds die de codec kent. In Quill zijn dit elk één `\uFFFC`-teken; de kaart telt ze als volledige tekstregels: - ```` ```mermaid ````-blokken: de kaart telt elke regel in de fence als zichtbare tekst (`_fencePattern` → `_Row.text`), Quill maakt er één `EmbeddableMermaid`-embed van → per diagram tientallen tekens verschil. - `<!-- toc -->`, `<!-- timeline -->`, `<!-- pentest … -->`-markers en voetnootdefinities `[^1]: …` → embeds, kaart telt ze als tekst. - inline `[^label]`-verwijzingen en inline `![alt](src)`-afbeeldingen → 1 teken in Quill, meerdere in de kaart (`stripInlineMarkdown` houdt `[^1]` letterlijk, `footnotes:false`). Elke zo'n constructie boven de cursor laat `sourceOffsetOf` systematisch te vroeg landen → A/C wijzen de verkeerde kop aan, terwijl B wel goed zit — of andersom. **Waarom traag:** per toetsaanslag lopen `toPlainText`, `MarkdownCaretMap.of`, `toDelta`, `markdownFromDocument`, tot 3× `buildMarkdownOutline`, een provider-edit én tot 3 losse post-frame `setState`s op de scherm-state — en `_documentOutlineRail` roept bij élke rebuild zelf weer `buildMarkdownOutline(source)` aan (`document_editor_toolbar.dart:819`). Alles O(document), per aanslag, meerdere keren. ## Reproductie (widget-test, bewezen) ```dart // Document met een mermaid-blok tussen twee koppen; caret in de paragraaf // eronder; per aanslag de actieve rail-kop uitlezen: const doc = '# Intro\n\n' '```mermaid\ngraph TD; A-->B; B-->C; C-->D;\n```\n\n' '| A | B |\n| --- | --- |\n| x | 1 |\n\n' '## Paragraaf\n\neerste regel\n\n## Slot\n\neinde\n'; // Metingen (typen van "regel toevoegen" + losse selectiemeldingen): // mermaid: [Intro, Paragraaf*, Intro, Paragraaf*, Intro, Paragraaf*, …] // opmaakkop: [Intro, Paragraaf **vet***, Intro, Intro*, …] // (* = na selectie-only melding) ``` Zonder zo'n divergentie blijft de markering stabiel (`[Paragraaf, Paragraaf, …]`) — de controle slaagt. ## Oplossingsrichting 1. **Eén berekening, één schrijver.** Kies één pad voor de actieve kop in de visuele stand en laat alleen dat pad `_setActiveOutlineIndex` aanroepen. De platte-tekstvariant (`B`) volgt de werkelijke Quill-caret zonder kwetsbare offset-vertaling — maar moet dan wel om koptitels met opmaak heen kunnen (zoek op `stripInlineMarkdown(title)`, of werk met blok-indexen i.p.v. `indexOf` op de titelstring). De bron-offsetvariant (`A/C`) is betrouwbaar zodra `MarkdownCaretMap` alle embed-types als object markeert — dat is dan de andere helft van de klus. Beide tegelijk laten schrijven is hoe dan ook de bug. 2. **`MarkdownCaretMap` bijwerken voor de embed-types**: mermaid-fences, `<!-- toc/timeline/pentest -->`-markers, voetnootdefinities en inline voetnoot/afbeelding-embeds als 1-teken objecten meetellen, zodat bron-offset↔visueel-offset weer klopt in documenten die ze bevatten. 3. **Kosten terugdringen**: `buildMarkdownOutline` niet 3–4× per aanslag draaien — één keer per bronwijziging cachen (of op de `MarkdownSourceDocument`/provider laten meerekenen) en de rail die versie laten gebruiken; en overwegen de outline-sync te dedupen per frame (max. één berekening per frame i.p.v. per notificatie). 4. Regressietest: een document met een mermaid-blok of opmaakkop boven de cursor; na typen mag `activeOutlineIndex` niet alterneren — één stabiele kop per frame.
brenno 2026-09-20 23:36:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#2141
No description provided.