krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 064f21f012

064f21f012fb0bf31ceb476120ebee93b5731477

parent: ef12a61bb3

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-05 02:57 UTC

Add paragraph style sharing to the viewer plan

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
1612git add Package.swift README.md Sources/Orgstar scripts 1612git add Package.swift README.md Sources/Orgstar scripts
1613git commit -m "Add the read-only viewer app" 1613git commit -m "Add the read-only viewer app"
1614``` 1614```
1615
1616---
1617
1618### Task 4: Share paragraph styles
1619
1620Per-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
1679Run: `ORGSTAR_GATES=1 ORGSTAR_GATE_FILE=<large real file> swift test -c release --filter GateTests`
1680
1681```bash
1682git add Sources/OrgEditorAppKit/OrgEditor.swift
1683git commit -m "Share paragraph styles across lines"
1684```