docs(renderer): document rendering determinism
This commit is contained in:
parent
fcfe65050f
commit
df1e2ad8f4
1 changed files with 139 additions and 0 deletions
|
|
@ -1018,6 +1018,145 @@ The rendered element architecture can support images, wiki links, Mermaid, LaTeX
|
||||||
|
|
||||||
Future rich elements will still need feature-specific parsers, layout, accessibility, hit testing, and caching. Those are additions to the rendered element set and platform widget layer, not a replacement of the hybrid editor architecture.
|
Future rich elements will still need feature-specific parsers, layout, accessibility, hit testing, and caching. Those are additions to the rendered element set and platform widget layer, not a replacement of the hybrid editor architecture.
|
||||||
|
|
||||||
|
## Finding #16 — Rendering Determinism
|
||||||
|
|
||||||
|
Milestone 3.3 separates the pure document render model from the editor presentation overlay. The previous lifecycle was explicit enough to decide source versus rendered lines, but it still lacked a testable model for the statement "same Markdown produces same rendered document." That made interaction-related regressions hard to isolate from real Markdown rendering changes.
|
||||||
|
|
||||||
|
Root causes found:
|
||||||
|
|
||||||
|
- Initial heading correctness depended on presentation passes being exercised through the editor path. There was no independent render model that could prove headings existed in the rendered document before focus, click, or selection changes.
|
||||||
|
- Rendered output and active-line presentation were easy to conflate. `DocumentPresentationState` intentionally depends on the active line because hybrid editing needs one source line, but the rendered document should not depend on active-line history.
|
||||||
|
- Render validation compared presentation behavior, not semantic render output. It could prove "line 2 is source" but not "this document deterministically contains heading, task, link, and code-block render nodes."
|
||||||
|
|
||||||
|
The corrected model is:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Markdown["Markdown source"] --> Index["DocumentLineIndex"]
|
||||||
|
Index --> Model["DocumentRenderModel"]
|
||||||
|
Model --> Nodes["RenderNode values"]
|
||||||
|
Model --> Snapshot["RenderSnapshot"]
|
||||||
|
Index --> Presentation["DocumentPresentationState"]
|
||||||
|
Selection["Current editor selection"] --> Presentation
|
||||||
|
Presentation --> TextKit["Hybrid NSTextView presentation"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`DocumentRenderModel` is the pure path. It receives a `DocumentLineIndex`, renders every selected line with no active line, and emits:
|
||||||
|
|
||||||
|
- `RenderedLine` values containing the source line, render plan, and semantic render nodes for that line.
|
||||||
|
- `RenderNode` values wrapping `RenderedDocumentElement` instances such as headings, tasks, links, inline code, blockquotes, list items, table rows, and code blocks.
|
||||||
|
- `RenderSnapshot`, a stable text signature of line kinds, spans, semantic nodes, source ranges, and whole-tree nodes.
|
||||||
|
|
||||||
|
`DocumentPresentationState` remains the editor path. It may depend on the current selection because active-line source mode is intentional. That interaction state is now outside the pure render model.
|
||||||
|
|
||||||
|
Determinism guarantees:
|
||||||
|
|
||||||
|
- Same Markdown source produces the same `DocumentRenderModel.snapshot`.
|
||||||
|
- Heading render nodes are produced on initial model construction without requiring focus, click, scroll, or selection changes.
|
||||||
|
- Snapshot changes are tied to Markdown changes, such as toggling `[ ]` to `[x]`.
|
||||||
|
- Dirty-line invalidation remains scoped to nearby affected lines; the pure render model does not add document-wide work to normal typing.
|
||||||
|
|
||||||
|
Validation added:
|
||||||
|
|
||||||
|
- `testDocumentRenderModelIsIndependentOfActiveLinePresentation`
|
||||||
|
- `testInitialRenderModelContainsHeadingsWithoutInteraction`
|
||||||
|
- `testRenderSnapshotChangesOnlyWhenMarkdownChanges`
|
||||||
|
- `testRenderedTaskToggleReplacementDoesNotMoveSourceRanges`
|
||||||
|
|
||||||
|
Release benchmark after Milestone 3.3:
|
||||||
|
|
||||||
|
| Scenario | Total | Dirty typing render | Dirty lines |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| sample document | 11.207 ms | 0.268 ms | 3 |
|
||||||
|
| 2,100-line prototype | 426.829 ms | 0.357 ms | 3 |
|
||||||
|
| 5 MB benchmark | 3,154.725 ms | 0.888 ms | 3 |
|
||||||
|
|
||||||
|
The interaction path remains inside the Milestone 2.9/3.2 responsiveness envelope. The 5 MB benchmark still performs dirty typing render under 1 ms and styles only three lines.
|
||||||
|
|
||||||
|
Remaining nondeterminism boundary:
|
||||||
|
|
||||||
|
- Semantic rendering is deterministic and covered by snapshots.
|
||||||
|
- Hybrid presentation intentionally changes with the current selection because the active line must show Markdown source.
|
||||||
|
- Pixel-level TextKit layout and overlay geometry are not yet covered by automated screenshot comparison. They are constrained by source ranges and native layout APIs, but not proven by a visual snapshot test.
|
||||||
|
- iOS still uses the attributed checklist fallback; the interactive overlay is macOS-only for now.
|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
|
||||||
|
The renderer now has a deterministic semantic model. The editor presentation is a deterministic projection of document state plus the current active-line selection, which is the intended hybrid editing exception.
|
||||||
|
|
||||||
|
## Finding #17 — Interactive Widget Stability
|
||||||
|
|
||||||
|
Milestone 3.3 stabilizes checklist widgets so they behave as document interactions instead of editing interactions.
|
||||||
|
|
||||||
|
The observed checklist bug was:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Before click:
|
||||||
|
☐ Move with arrow keys.
|
||||||
|
|
||||||
|
After click:
|
||||||
|
☐
|
||||||
|
|
||||||
|
Move with arrow keys.
|
||||||
|
```
|
||||||
|
|
||||||
|
Root cause:
|
||||||
|
|
||||||
|
The checkbox toggle path replaced the Markdown checkbox and then moved the text selection to the checkbox range. That made the task line become the active source line after the click. Because active lines show Markdown source and inactive lines show rendered content, the click changed line state and layout geometry even though the user only intended to toggle task state.
|
||||||
|
|
||||||
|
The corrected interaction is:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Checkbox as Checklist overlay
|
||||||
|
participant TextView as NSTextView
|
||||||
|
participant Source as Markdown source
|
||||||
|
participant Index as DocumentLineIndex
|
||||||
|
participant Renderer as Presentation renderer
|
||||||
|
|
||||||
|
User->>Checkbox: click
|
||||||
|
Checkbox->>TextView: preserve current selection and focus state
|
||||||
|
TextView->>Source: replace [ ] with [x] or [x] with [ ]
|
||||||
|
TextView->>Index: apply same-length DocumentLineIndexEdit
|
||||||
|
TextView->>Renderer: rerender affected task line
|
||||||
|
Renderer->>Checkbox: update checkbox state at same source range
|
||||||
|
TextView->>TextView: restore previous selection and focus
|
||||||
|
```
|
||||||
|
|
||||||
|
Checklist stability guarantees:
|
||||||
|
|
||||||
|
- Clicking a rendered checkbox updates only the Markdown checkbox marker.
|
||||||
|
- The replacement is same-length (`[ ]` and `[x]`), so source ranges and layout anchors remain stable.
|
||||||
|
- The previous selection is restored after the programmatic edit.
|
||||||
|
- The text view regains first responder only when it already had focus before the click.
|
||||||
|
- The native checkbox refuses first responder, preventing the control from stealing editor focus.
|
||||||
|
- `pendingEdit` is bypassed during the programmatic toggle so the line index receives exactly one explicit `DocumentLineIndexEdit`.
|
||||||
|
|
||||||
|
This preserves the active editing line. A user can edit one paragraph, click a checklist elsewhere, and continue editing from the same selection.
|
||||||
|
|
||||||
|
Paragraph stability:
|
||||||
|
|
||||||
|
The paragraph reflow symptom was caused by widget clicks changing the active line, not by different paragraph Markdown. Once checkbox clicks preserve selection and active line, unchanged paragraphs continue through the same rendered/source state and therefore keep the same wrapping, alignment, and geometry.
|
||||||
|
|
||||||
|
Tradeoffs:
|
||||||
|
|
||||||
|
- The widget remains an overlay rather than a replacement inside the text buffer. This preserves native source selection, undo integration, IME behavior, and dirty-line invalidation.
|
||||||
|
- The checkbox toggle still modifies Markdown source immediately; undo semantics should continue through the native text system, but richer widget undo behavior needs manual testing as more widgets are added.
|
||||||
|
- Visual stability is constrained by same-length source edits and range-based overlay placement. Pixel-level regression coverage remains future work.
|
||||||
|
|
||||||
|
Applicability to future widgets:
|
||||||
|
|
||||||
|
The same interaction contract should apply to images, wiki links, embeds, Mermaid, and LaTeX:
|
||||||
|
|
||||||
|
- Render from semantic nodes.
|
||||||
|
- Keep Markdown as source of truth.
|
||||||
|
- Route widget actions through explicit source edits.
|
||||||
|
- Preserve selection, active line, and focus unless the user explicitly begins editing that element.
|
||||||
|
- Rerender only affected source ranges.
|
||||||
|
|
||||||
|
This means future rendered widgets can be added without changing the fundamental editor architecture.
|
||||||
|
|
||||||
## AttributedString and NSAttributedString
|
## AttributedString and NSAttributedString
|
||||||
|
|
||||||
Swift `AttributedString` is useful for renderer-facing APIs and SwiftUI previews.
|
Swift `AttributedString` is useful for renderer-facing APIs and SwiftUI previews.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue