docs(renderer): document rendered lifecycle
This commit is contained in:
parent
0dd8351847
commit
f4948c4089
1 changed files with 166 additions and 0 deletions
|
|
@ -852,6 +852,172 @@ Conclusion:
|
||||||
|
|
||||||
Milestone 3.1 moves Sapling's inactive-line display closer to rendered Markdown while preserving the validated hybrid editing contract. The renderer now hides the most distracting source syntax for headings, inline markup, and fenced code blocks, and it gives task lists visible state. The remaining limitations are deliberate source-preserving tradeoffs rather than missed parser features.
|
Milestone 3.1 moves Sapling's inactive-line display closer to rendered Markdown while preserving the validated hybrid editing contract. The renderer now hides the most distracting source syntax for headings, inline markup, and fenced code blocks, and it gives task lists visible state. The remaining limitations are deliberate source-preserving tradeoffs rather than missed parser features.
|
||||||
|
|
||||||
|
## Finding #14 — Rendered Document Lifecycle
|
||||||
|
|
||||||
|
Milestone 3.2 makes rendered state explicit instead of leaving it implicit inside styling code. Before this change, the native adapter decided whether a line was source or rendered inside `MarkdownTextStyler.apply(...)` by comparing each line index to the active line. That worked, but it left presentation state spread across delegate timing, invalidation caches, and styler branches.
|
||||||
|
|
||||||
|
The new lifecycle model is:
|
||||||
|
|
||||||
|
- `RenderedLineState.source`: exactly one active line derived from the current native selection and `DocumentLineIndex`.
|
||||||
|
- `RenderedLineState.rendered`: every other visible line.
|
||||||
|
- `DocumentPresentationState`: deterministic projection from `(DocumentLineIndex, activeLineIndex, optional dirty line indexes)` to presentation lines and semantic rendered elements.
|
||||||
|
- `DocumentPresentationLine`: one source line plus its line state, render plan, and semantic rendered elements.
|
||||||
|
|
||||||
|
Every line in a presentation pass is now in exactly one state. The state is data-driven by current document text and current selection, not by previous focus history or click history.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Rendered: line.index != activeLineIndex
|
||||||
|
[*] --> Source: line.index == activeLineIndex
|
||||||
|
Rendered --> Source: selection enters line
|
||||||
|
Source --> Rendered: selection leaves line
|
||||||
|
Rendered --> Rendered: source edit elsewhere
|
||||||
|
Source --> Source: source edit on active line
|
||||||
|
```
|
||||||
|
|
||||||
|
Render pipeline after Milestone 3.2:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
NativeSelection["Native text selection"] --> Active["DocumentLineIndex.lineIndex(containing:)"]
|
||||||
|
Source["Markdown source"] --> LineIndex["DocumentLineIndex"]
|
||||||
|
LineIndex --> Presentation["DocumentPresentationState"]
|
||||||
|
Active --> Presentation
|
||||||
|
Dirty["Dirty line indexes"] --> Presentation
|
||||||
|
Presentation --> Lines["DocumentPresentationLine"]
|
||||||
|
Lines --> Styler["MarkdownTextStyler"]
|
||||||
|
Lines --> Elements["RenderedDocumentElement"]
|
||||||
|
Elements --> Widgets["Rendered widgets / overlays"]
|
||||||
|
Styler --> TextStorage["NSTextStorage attributes"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Invalidation audit:
|
||||||
|
|
||||||
|
- Initial rendering is still a full presentation pass. `EditorDirtyLineInvalidator` emits `.initial` with every line index, and the presentation model assigns one source line and all remaining lines as rendered.
|
||||||
|
- Source edits still use dirty-line invalidation and preserve neighbor-line coverage for constructs affected by adjacent text.
|
||||||
|
- Active-line transitions still dirty only the previous and current active line.
|
||||||
|
- View updates with unchanged source and unchanged active line still skip styling.
|
||||||
|
- The important timing fix is that native adapters now derive the active line from the current native text selection immediately before building the invalidation plan. Selection-change styling no longer depends on waiting for SwiftUI binding propagation to update the `activeLineIndex` value.
|
||||||
|
|
||||||
|
Rendered element architecture:
|
||||||
|
|
||||||
|
`DocumentPresentationState` now emits semantic `RenderedDocumentElement` values for supported constructs:
|
||||||
|
|
||||||
|
- `heading`
|
||||||
|
- `task`
|
||||||
|
- `codeBlock`
|
||||||
|
- `link`
|
||||||
|
- `inlineCode`
|
||||||
|
- `blockquote`
|
||||||
|
- `horizontalRule`
|
||||||
|
- `listItem`
|
||||||
|
- `tableRow`
|
||||||
|
|
||||||
|
Plain paragraphs are intentionally not materialized as semantic elements in normal passes. They are represented by their `DocumentPresentationLine` and render plan, which avoids allocating tens of thousands of no-op paragraph elements in large documents.
|
||||||
|
|
||||||
|
Code block lifecycle:
|
||||||
|
|
||||||
|
Fenced code blocks are represented as semantic multi-line `RenderedCodeBlockElement` values with:
|
||||||
|
|
||||||
|
- all participating line indexes
|
||||||
|
- the source range spanning the block
|
||||||
|
- the optional language range
|
||||||
|
|
||||||
|
The attributed renderer still styles code blocks line-by-line inside TextKit, but the document presentation model now has a block-level representation that future widgets can consume without reparsing source text.
|
||||||
|
|
||||||
|
Tradeoffs:
|
||||||
|
|
||||||
|
- Text remains source-preserving. Hidden syntax is still done with attributes, not source replacement.
|
||||||
|
- The native text view remains the scroll, selection, undo, and IME owner.
|
||||||
|
- Semantic elements are only emitted for constructs that need rendered behavior or future widget hooks.
|
||||||
|
- Dirty presentation only resolves code-block context when nearby fence syntax makes block context relevant; this preserves the Milestone 2.9 dirty-render scalability model.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `DocumentPresentationStateTests` verify one state per line, deterministic projection for identical input, semantic heading/task/link elements, semantic code-block state, and dirty code-block context recovery.
|
||||||
|
- Existing cursor, dirty invalidation, scroll stability, rendering, large-document, and performance tests continue to pass.
|
||||||
|
|
||||||
|
Before/after release benchmark comparison:
|
||||||
|
|
||||||
|
| Scenario | Milestone 3.1 total | Milestone 3.2 total | Milestone 3.1 dirty typing render | Milestone 3.2 dirty typing render |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| sample document | 13.506 ms | 10.013 ms | 0.330 ms | 0.220 ms |
|
||||||
|
| 2,100-line prototype | 423.835 ms | 477.423 ms | 0.368 ms | 0.383 ms |
|
||||||
|
| 5 MB benchmark | 3,275.857 ms | 3,614.818 ms | 1.002 ms | 1.049 ms |
|
||||||
|
|
||||||
|
Tracked 5 MB interaction metrics after Milestone 3.2:
|
||||||
|
|
||||||
|
| Operation | Time |
|
||||||
|
| --- | ---: |
|
||||||
|
| active-line lookup | 0.000 ms |
|
||||||
|
| selection update | 0.005 ms |
|
||||||
|
| dirty click invalidation | 0.001 ms |
|
||||||
|
| typing state update | 0.077 ms |
|
||||||
|
| dirty typing invalidation | 0.003 ms |
|
||||||
|
| dirty typing render | 1.049 ms |
|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
|
||||||
|
The presentation lifecycle is now explicit and deterministic. Opening a document, moving the cursor, and editing source all project through the same state model: source for the active line, rendered for inactive lines. The measured cost is a modest increase in initial large-document work, while dirty interaction remains within the Milestone 2.9 responsiveness envelope.
|
||||||
|
|
||||||
|
## Finding #15 — Interactive Rendered Elements
|
||||||
|
|
||||||
|
Milestone 3.2 introduces task lists as the first interactive rendered element. The Markdown source remains authoritative; the checkbox is only a rendered control backed by a known source range.
|
||||||
|
|
||||||
|
Checklist architecture on macOS:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant Checkbox as Native NSButton checkbox
|
||||||
|
participant Coordinator as NSTextView Coordinator
|
||||||
|
participant Source as Markdown source
|
||||||
|
participant Index as DocumentLineIndex
|
||||||
|
participant Presentation as DocumentPresentationState
|
||||||
|
participant Styler as MarkdownTextStyler
|
||||||
|
|
||||||
|
User->>Checkbox: click
|
||||||
|
Checkbox->>Coordinator: toggle RenderedTaskElement
|
||||||
|
Coordinator->>Source: replace checkbox range with [ ] or [x]
|
||||||
|
Coordinator->>Index: apply DocumentLineIndexEdit
|
||||||
|
Coordinator->>Presentation: rebuild affected presentation
|
||||||
|
Presentation->>Styler: rendered/source line states
|
||||||
|
Styler->>Checkbox: sync overlay state and position
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation details:
|
||||||
|
|
||||||
|
- Rendered task lines produce `RenderedTaskElement` with marker, checkbox, content, checked state, and nesting level.
|
||||||
|
- The macOS `NSTextView` adapter creates native `NSButton` checkbox overlays for inactive rendered task lines.
|
||||||
|
- Clicking the checkbox replaces the Markdown checkbox source range with `[x]` or `[ ]`.
|
||||||
|
- The edit flows through the same source update, line-index update, dirty invalidation, and restyling path as typing.
|
||||||
|
- When the line becomes active, it returns to source mode and the overlay is removed for that line.
|
||||||
|
|
||||||
|
This is deliberately an overlay, not a text replacement. TextKit remains responsible for the source buffer and selection. The overlay consumes semantic rendered element data and writes back to source through `DocumentLineIndexEdit`.
|
||||||
|
|
||||||
|
Current platform scope:
|
||||||
|
|
||||||
|
- macOS has the native interactive checklist overlay.
|
||||||
|
- iOS keeps the attributed task-list fallback for now; it needs a `UITextView` overlay equivalent before task checkboxes are interactive there.
|
||||||
|
|
||||||
|
Consistency:
|
||||||
|
|
||||||
|
- Headings, inline Markdown, links, task lists, and code blocks now all pass through `DocumentPresentationState`.
|
||||||
|
- Active lines show source.
|
||||||
|
- Inactive lines show rendered presentation.
|
||||||
|
- Widgets are attached to semantic rendered elements, not to ad hoc regex matches in the native view.
|
||||||
|
|
||||||
|
Future extensibility:
|
||||||
|
|
||||||
|
The rendered element architecture can support images, wiki links, Mermaid, LaTeX, and embeds without fundamental redesign. The evidence is structural:
|
||||||
|
|
||||||
|
- The presentation layer already separates semantic element detection from platform rendering.
|
||||||
|
- Elements carry source ranges, so widgets can update or inspect Markdown while preserving source as truth.
|
||||||
|
- Code blocks already prove multi-line semantic elements can coexist with line-level dirty styling.
|
||||||
|
- Checklist overlays prove rendered controls can live above the source-preserving text view and write back through normal edit/invalidation paths.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## 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