# Editable TextKit 2 Spike Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Prove that a TextKit 2 text view can fold org subtrees and show org-indent while caret movement, selection, input methods, VoiceOver and copy/paste stay correct, with typing restyled inside a 16 ms frame. Find what doesn't work before phase 2 builds on it. **Architecture:** `OrgPresentation` is the platform-free layout policy: style runs, org-indent columns and hidden ranges, all computed from the tree. `OrgEditorAppKit` maps that policy onto an `NSTextView` with TextKit 2: folded paragraphs are skipped by `NSTextContentManagerDelegate.shouldEnumerate`, folded headings draw an ellipsis from a custom `NSTextLayoutFragment`, and every character edit flows from the text storage into `DocumentState.apply`. Only the affected region is restyled: the elements around the edit in the innermost section, computed on both the old and the new tree. `OrgstarSpike` is a throwaway app for trying it by hand. **Tech Stack:** Swift 6.2 tools, Swift Testing, AppKit, TextKit 2. **Spec:** `docs/design.md`, "iOS considerations" (editor view row) and the phase 1 editing spike gate. ## Global Constraints - `OrgPresentation` imports only `OrgCore`; it builds for iOS. - The text storage and `DocumentState` hold the same text after every edit, and the tree equals a full parse. - Incrementally restyled text equals a fresh editor's full styling after every edit. - Folded text stays in the storage: copy, accessibility and saving see all of it. ## Findings Measured in release builds on an M-series Mac (not yet the M1 Air reference machine). | Check | Result | | --- | --- | | Folding by skipping enumeration | Works; layout drops folded paragraphs; ellipsis drawn after folded headings | | Caret | `moveDown` skips folds; `moveRight`/`moveLeft` would enter hidden text, so the editor moves the caret across the fold | | Copy across a fold | Includes the hidden text | | Input methods | Marked text and commit keep the document in sync | | Undo (`NSTextView`'s undo manager) | Keeps the document in sync | | Accessibility | `accessibilityValue` is the whole file, folds included; selection and line queries work. A manual VoiceOver pass is still needed (below) | | Typing, p95 | 5.8 ms on a 1.5 MB file, 3.4 ms on 0.86 MB (gate 16 ms) | | Blank line in a 1.2 MB top-level section | 256 ms; the reparse covers the whole top-level section (gate 16 ms) | | Open: parse + load and style | 313 + 124 ms for 1.5 MB; 126 + 101 ms for 0.86 MB (gate 100 ms for 1 MB) | | Restyle correctness | 10,500 random edits over seven seeds match a fresh full styling | Two bugs the differential test found and the plan fixes: a line split off by an inserted newline kept the old line's style, and a block or drawer whose delimiters changed left stale styling below the edit (fixed by restyling the union of the old and new trees' affected ranges). Work for the performance plan: reparse structural edits within the innermost section instead of the top-level section; a faster parser (about 3x for the open gate); style the visible region first on open and the rest after. Manual VoiceOver checklist (run `swift run OrgstarSpike `, turn on VoiceOver): - [ ] Line by line through a folded heading skips to the next visible heading. - [ ] Reading a heading with tags and a TODO keyword is understandable. - [ ] Selection announcements are correct across a fold. ## File structure | File | Responsibility | | --- | --- | | `Sources/OrgPresentation/Presentation.swift` | Style roles and runs, indent runs, hidden ranges, heading lookup | | `Sources/OrgEditorAppKit/OrgEditor.swift` | TextKit 2 editor: sync, folding, caret, restyle region, save | | `Sources/OrgEditorAppKit/Theme.swift` | Attributes per style role | | `Sources/OrgstarSpike/main.swift` | Runnable spike app | | `Sources/OrgDocument/ViewState.swift` | `pruned(to:)` descends only through containing sections | --- ### Task 1: Presentation policy **Files:** - Modify: `Package.swift` (add `OrgPresentation`) - Create: `Sources/OrgPresentation/Presentation.swift` - Test: `Tests/OrgPresentationTests/PresentationTests.swift` **Interfaces:** - Produces: `StyleRole`, `StyleRun`, `IndentRun`, `Presentation.styles(_:in:)`, `Presentation.indents(_:in:)`, `Presentation.hiddenRanges(_:folds:)`, `Presentation.heading(containing:in:)`, `Presentation.headings(_:)`. - [ ] **Step 1: Write the failing tests** ```swift import OrgCore import Testing @testable import OrgPresentation func roles(_ text: String) -> [String] { let tree = OrgParser.parse(text) let utf16 = Array(text.utf16) return Presentation.styles(tree, in: 0..\n").contains("metadata:SCHEDULED: <2026-10-04 Sun>\n")) } @Test func onlyRunsOverlappingTheRange() { let text = "* a\nfirst *b*\n* c\nsecond *d*\n" let tree = OrgParser.parse(text) let runs = Presentation.styles(tree, in: 17..<28) #expect(!runs.contains { $0.range == 10..<13 }) #expect(runs.contains { $0.range == 25..<28 && $0.role == .bold }) } } struct IndentTests { @Test func bodiesIndentPastTheirStars() { let text = "pre\n* a\nbody\n** b\nmore\n" let runs = Presentation.indents(OrgParser.parse(text), in: 0..