Commit 064f21f012
Verified · cmc
Layout: unified · split
docs/plans/2026-10-04-viewer.md +70
| @@ -1612,3 +1612,73 @@ Manual: add a folder with ⇧⌘O; ⌘P opens a file; ⇧⌘F search opens resul | ||
| 1612 | 1612 | git add Package.swift README.md Sources/Orgstar scripts |
| 1613 | 1613 | git commit -m "Add the read-only viewer app" |
| 1614 | 1614 | ``` |
| 1615 | ||
| 1616 | --- | |
| 1617 | ||
| 1618 | ### Task 4: Share paragraph styles | |
| 1619 | ||
| 1620 | Per-line hanging indents gave every line its own `NSParagraphStyle`, which pushed a 1.5 MB file from 9.6x to 10.2x its size in memory, over the 10x gate. One style object per indent pair brings it back to 9.7x. | |
| 1621 | ||
| 1622 | **Files:** | |
| 1623 | - Modify: `Sources/OrgEditorAppKit/OrgEditor.swift` | |
| 1624 | ||
| 1625 | - [ ] **Step 1: Implement** | |
| 1626 | ||
| 1627 | ```diff | |
| 1628 | @@ -59,6 +59,13 @@ public final class OrgEditor: NSObject { | |
| 1629 | /// passes reach it. Always at a line start. | |
| 1630 | private(set) var styledUpTo = 0 | |
| 1631 | private var stylingScheduled = false | |
| 1632 | + /// One paragraph style object per indent pair, shared by every line that uses it. | |
| 1633 | + private var paragraphStyles: [IndentKey: NSParagraphStyle] = [:] | |
| 1634 | + | |
| 1635 | + private struct IndentKey: Hashable { | |
| 1636 | + let firstLine: Int | |
| 1637 | + let wrapped: Int | |
| 1638 | + } | |
| 1639 | ||
| 1640 | /// Styled before the first render: more than a screenful. | |
| 1641 | static let firstStyleChunk = 30_000 | |
| 1642 | @@ -253,7 +260,7 @@ public final class OrgEditor: NSObject { | |
| 1643 | for run in Presentation.indents(document.tree, in: range) { | |
| 1644 | guard let clipped = clip(run.range, to: range) else { continue } | |
| 1645 | guard run.firstLine == run.wrapped else { | |
| 1646 | - storage.addAttribute(.paragraphStyle, value: theme.paragraphStyle(firstLine: run.firstLine, wrapped: run.wrapped), range: clipped) | |
| 1647 | + storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped), range: clipped) | |
| 1648 | continue | |
| 1649 | } | |
| 1650 | // Body lines wrap under their own text: past leading blanks, and past a list bullet | |
| 1651 | @@ -263,7 +270,7 @@ public final class OrgEditor: NSObject { | |
| 1652 | let line = text.lineRange(for: NSRange(location: location, length: 0)) | |
| 1653 | let hanging = Self.hangingColumns(text.substring(with: line)) | |
| 1654 | let lineRange = NSIntersectionRange(line, clipped) | |
| 1655 | - storage.addAttribute(.paragraphStyle, value: theme.paragraphStyle(firstLine: run.firstLine, wrapped: run.wrapped + hanging), range: lineRange) | |
| 1656 | + storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped + hanging), range: lineRange) | |
| 1657 | location = NSMaxRange(line) | |
| 1658 | } | |
| 1659 | } | |
| 1660 | @@ -273,6 +280,14 @@ public final class OrgEditor: NSObject { | |
| 1661 | } | |
| 1662 | } | |
| 1663 | ||
| 1664 | + private func paragraphStyle(_ firstLine: Int, _ wrapped: Int) -> NSParagraphStyle { | |
| 1665 | + let key = IndentKey(firstLine: firstLine, wrapped: wrapped) | |
| 1666 | + if let style = paragraphStyles[key] { return style } | |
| 1667 | + let style = theme.paragraphStyle(firstLine: firstLine, wrapped: wrapped) | |
| 1668 | + paragraphStyles[key] = style | |
| 1669 | + return style | |
| 1670 | + } | |
| 1671 | + | |
| 1672 | private func clip(_ run: Range<Int>, to range: Range<Int>) -> NSRange? { | |
| 1673 | let clipped = run.clamped(to: range) | |
| 1674 | return clipped.isEmpty ? nil : NSRange(clipped) | |
| 1675 | ``` | |
| 1676 | ||
| 1677 | - [ ] **Step 2: Run the gates; commit** | |
| 1678 | ||
| 1679 | Run: `ORGSTAR_GATES=1 ORGSTAR_GATE_FILE=<large real file> swift test -c release --filter GateTests` | |
| 1680 | ||
| 1681 | ```bash | |
| 1682 | git add Sources/OrgEditorAppKit/OrgEditor.swift | |
| 1683 | git commit -m "Share paragraph styles across lines" | |
| 1684 | ``` | |