diff --git a/Docs/editor-investigation.md b/Docs/editor-investigation.md new file mode 100644 index 0000000..fafde78 --- /dev/null +++ b/Docs/editor-investigation.md @@ -0,0 +1,243 @@ +# Editor Investigation + +Date: 2026-05 + +Status: Milestone 1 validation + +Related decisions: + +- D-008: Hybrid Markdown Editing Is a Core Feature +- D-009: The Editor Is the Highest-Priority System +- D-010: Git Access Must Be Abstracted +- D-012: Platform Focus +- D-013: Editor Technology Selection + +## Question + +Can Sapling support a high-quality hybrid Markdown editing experience where the active line shows Markdown source and inactive lines show rendered Markdown? + +## Prototype Summary + +The Milestone 1 prototype uses a SwiftUI application shell with a native text view bridge: + +- macOS: `NSTextView` through `NSViewRepresentable` +- iOS path: `UITextView` through `UIViewRepresentable` +- shared editor state in `SaplingEditor` +- document loading and saving through `HybridMarkdownEditorViewModel` +- line tracking through `EditorState`, `EditorLine`, and `EditorSelection` +- in-place line styling through `NSTextStorage` + +The prototype validates that native text views can support the first version of Sapling's editor without coupling the app directly to AppKit. + +## Current Architecture + +```mermaid +flowchart TD + App["SwiftUI app shell"] --> Editor["HybridMarkdownEditor"] + Editor --> Bridge["Native text view bridge"] + Bridge --> Mac["NSTextView on macOS"] + Bridge --> IOS["UITextView on iOS"] + Editor --> Coordinator["EditorCoordinator"] + Coordinator --> State["EditorState"] + State --> Document["EditorDocument"] + State --> Lines["EditorLine array"] + State --> Selection["EditorSelection"] + Bridge --> TextKit["Text storage and selection APIs"] +``` + +The important boundary is the Sapling editor abstraction. Application code talks to `HybridMarkdownEditorViewModel` and editor state, not to `NSTextView`. + +## Architectures Explored + +### SwiftUI TextEditor + +SwiftUI `TextEditor` was already present in the early prototype and worked for simple active-line editing. + +Benefits: + +- Minimal SwiftUI integration. +- Fast to prototype. +- Good enough for plain text entry. + +Limitations: + +- Does not expose reliable low-level selection and cursor APIs. +- Does not provide direct TextKit access. +- Makes line-level source/render switching difficult. +- Offers limited control over attributed rendering and layout. + +Conclusion: `TextEditor` is not suitable as Sapling's primary editor implementation. + +### NSTextView With Attributed Styling + +The implemented spike keeps Markdown source as the actual text buffer and applies visual styling to inactive lines with `NSTextStorage`. + +Benefits: + +- Native cursor movement remains intact. +- Native selection ranges remain tied to the real source buffer. +- Undo, find, spell checking, and keyboard navigation come from AppKit. +- Active line detection can be derived from selection location. +- Inactive lines can be styled differently from the active source line. + +Limitations: + +- Styling alone cannot remove Markdown delimiters without affecting layout. +- It cannot render block elements such as images, tables, or Mermaid diagrams as real embedded views. +- Changing font sizes per line affects line height and may create visual jumps. +- Reapplying attributes on every selection change must be optimized for large documents. + +Conclusion: This is sufficient for Milestone 1 and likely sufficient for an early Milestone 2 implementation of headings, emphasis, links, lists, and task lists. + +### Line Replacement + +Line replacement would swap inactive Markdown source with rendered text while preserving the source elsewhere. + +Benefits: + +- More closely matches the desired hybrid model. +- Can hide Markdown delimiters from inactive lines. + +Risks: + +- Cursor offsets no longer map directly to source offsets. +- Selection crossing active and inactive lines becomes complex. +- Undo behavior may become fragile. +- TextKit may fight source/render buffer divergence. + +Conclusion: Worth exploring after the attributed-string path, but risky as the primary approach. + +### Overlay Rendering + +Overlay rendering would keep source text in the editor but draw rendered content above inactive lines. + +Benefits: + +- Source buffer remains canonical. +- Rendered views could support richer blocks. +- Cursor math can still use the real text buffer. + +Risks: + +- Requires precise layout synchronization with TextKit. +- Hit testing and selection feedback are difficult. +- Accessibility must be designed carefully. + +Conclusion: Promising for richer Milestone 3 rendering, but heavier than needed for Milestone 2. + +### Mixed Text and Render Layers + +A custom layout could combine text editing lines with rendered SwiftUI/AppKit views. + +Benefits: + +- Maximum control over rendering. +- Can support images, diagrams, tables, and custom blocks. + +Risks: + +- Requires building editor infrastructure Sapling does not yet have. +- Cursor movement, selection, IME, undo, accessibility, and text input become product-critical systems. + +Conclusion: This may be necessary in the long term, but it should not be the first Milestone 2 implementation. + +## TextKit Findings + +`NSTextView` gives Sapling the hooks needed for the prototype: + +- `selectedRange()` gives the cursor and selection range. +- `NSTextStorage` allows line-level attributes without replacing source text. +- `NSTextViewDelegate` reports text and selection changes. +- Native editing behavior remains available while the app tracks editor state. + +The key finding is that Sapling can preserve a single source buffer and still present different active and inactive line states. This reduces risk compared with maintaining separate source and render buffers. + +## AttributedString and NSAttributedString + +Swift `AttributedString` is useful for renderer-facing APIs and SwiftUI previews. + +`NSAttributedString` and `NSTextStorage` are still required for the actual native editor bridge because the text view operates on TextKit types. + +Recommended split: + +- Use `AttributedString` in cross-platform renderer interfaces. +- Use `NSAttributedString` and `NSTextStorage` inside platform adapters. +- Keep conversion and styling code contained in `SaplingEditor`. + +## Cursor Challenges + +The prototype validates basic active-line detection: + +- The selected range location maps to a line index. +- The active line switches as the cursor moves. +- The active line can be styled as source while other lines are styled as rendered. + +Open challenges: + +- Multi-line selection across source and rendered lines. +- Selection after visually hidden or replaced Markdown delimiters. +- Cursor placement around links, emphasis markers, and task markers. +- IME composition and marked text handling. +- Very large documents where full restyling on each selection change is too expensive. + +Cursor correctness is the main reason to keep the canonical source text inside `NSTextView` for now. + +## Rendering Challenges + +The current spike uses line-level styling, not full Markdown rendering. + +Validated: + +- Headings can be styled by line. +- Emphasis and links can be styled inline. +- Task markers can be visually deemphasized. +- Active source line can remain monospaced and visibly distinct. + +Not validated: + +- True delimiter removal without cursor side effects. +- Embedded rendered blocks inside the text layout. +- Images, tables, code block widgets, Mermaid, LaTeX, and attachments. +- Precise layout matching between rendered inactive lines and raw active lines. + +## Hybrid Editing Feasibility + +Hybrid editing is realistically achievable for Milestone 2 if the first implementation is scoped to line-level attributed styling. + +The safest path is: + +1. Keep the Markdown source as the only editable text buffer. +2. Detect the active line from the native selection range. +3. Apply source styling to the active line. +4. Apply rendered-style attributes to inactive lines. +5. Delay true line replacement and overlay rendering until selection behavior is better understood. + +This will not produce perfect rendered Markdown, but it should validate the writing feel and cursor behavior before Sapling invests in a custom editor engine. + +## Recommended Path Forward + +Use `NSTextView` for Milestone 2 on macOS and keep the iOS `UITextView` adapter available behind the same editor abstraction. + +Immediate next steps: + +- Move Markdown styling into a dedicated renderer adapter. +- Add incremental line invalidation instead of restyling the whole buffer. +- Add tests for line ranges, trailing newlines, and multi-line selections. +- Manually test long documents, undo/redo, IME input, and keyboard navigation. +- Explore delimiter hiding with temporary attributes only after selection mapping is robust. + +Future investigation: + +- Use TextKit layout fragments or overlay views for rich block rendering. +- Prototype image and code block rendering outside the text buffer. +- Reevaluate whether a custom editor engine is required after Milestone 2. + +## Decision + +`NSTextView` is sufficient for Milestone 2 validation. + +Hybrid editing is achievable, but the near-term implementation should be attribute-based rather than replacement-based. + +A future custom editor engine may be required for full rich block rendering, but Sapling should not start there. + +The architecture should continue with a SwiftUI shell, a Sapling editor abstraction, and native platform text views hidden behind replaceable adapters.