| @@ -52,6 +52,8 @@ public final class OrgEditor: NSObject { |
| 52 | |
52 | |
| 53 | let hidden = HiddenRanges() |
53 | let hidden = HiddenRanges() |
| 54 | let folded = FoldedHeadings() |
54 | let folded = FoldedHeadings() |
| |
55 | /// Blocks drawn on a shaded band. |
| |
56 | let shaded = HiddenRanges() |
| 55 | let theme = Theme() |
57 | let theme = Theme() |
| 56 | private var isLoading = false |
58 | private var isLoading = false |
| 57 | private var previousSelection = 0 |
59 | private var previousSelection = 0 |
| @@ -121,6 +123,7 @@ public final class OrgEditor: NSObject { |
| 121 | textLayoutManager.delegate = self |
123 | textLayoutManager.delegate = self |
| 122 | textContentStorage.delegate = self |
124 | textContentStorage.delegate = self |
| 123 | |
125 | |
| |
126 | if isOrg { shaded.set(Presentation.shadedBlocks(document.tree)) } |
| 124 | load(document.text) |
127 | load(document.text) |
| 125 | if isOrg, let startup = Presentation.startupFoldState(document.tree) { setFolds(startup) } |
128 | if isOrg, let startup = Presentation.startupFoldState(document.tree) { setFolds(startup) } |
| 126 | } |
129 | } |
| @@ -653,7 +656,10 @@ public final class OrgEditor: NSObject { |
| 653 | let delta = edit.replacement.utf16.count - edit.range.count |
656 | let delta = edit.replacement.utf16.count - edit.range.count |
| 654 | let oldMapped = oldAffected.lowerBound..<(oldAffected.upperBound >= edit.range.upperBound ? oldAffected.upperBound + delta : newEnd) |
657 | let oldMapped = oldAffected.lowerBound..<(oldAffected.upperBound >= edit.range.upperBound ? oldAffected.upperBound + delta : newEnd) |
| 655 | restyle(min(oldMapped.lowerBound, newAffected.lowerBound)..<min(max(oldMapped.upperBound, newAffected.upperBound), newText.length), in: storage) |
658 | restyle(min(oldMapped.lowerBound, newAffected.lowerBound)..<min(max(oldMapped.upperBound, newAffected.upperBound), newText.length), in: storage) |
| 656 | if hidden.all != oldHidden.map({ mapRange($0, through: edit) }) { |
659 | let oldShaded = shaded.all |
| |
660 | if isOrg { shaded.set(Presentation.shadedBlocks(document.tree)) } |
| |
661 | if hidden.all != oldHidden.map({ mapRange($0, through: edit) }) |
| |
662 | || shaded.all != oldShaded.map({ mapRange($0, through: edit) }) { |
| 657 | textLayoutManager.invalidateLayout(for: textLayoutManager.documentRange) |
663 | textLayoutManager.invalidateLayout(for: textLayoutManager.documentRange) |
| 658 | } |
664 | } |
| 659 | onEditTiming?(clock.now - start) |
665 | onEditTiming?(clock.now - start) |
| @@ -799,11 +805,31 @@ extension OrgEditor: NSTextLayoutManagerDelegate { |
| 799 | if let range, let manager = textLayoutManager.textContentManager { |
805 | if let range, let manager = textLayoutManager.textContentManager { |
| 800 | let offset = manager.offset(from: manager.documentRange.location, to: range.location) |
806 | let offset = manager.offset(from: manager.documentRange.location, to: range.location) |
| 801 | if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } |
807 | if folded.contains(offset) { return FoldedHeadingFragment(textElement: textElement, range: range) } |
| |
808 | if shaded.containing(offset) != nil { return ShadedFragment(textElement: textElement, range: range) } |
| 802 | } |
809 | } |
| 803 | return NSTextLayoutFragment(textElement: textElement, range: range) |
810 | return NSTextLayoutFragment(textElement: textElement, range: range) |
| 804 | } |
811 | } |
| 805 | } |
812 | } |
| 806 | |
813 | |
| |
814 | /// A line of a block, on a band across the whole width of the view, as Emacs extends the |
| |
815 | /// `org-block` face. |
| |
816 | final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
| |
817 | private var band: CGRect { |
| |
818 | let width = textLayoutManager?.textContainer?.size.width ?? layoutFragmentFrame.width |
| |
819 | return CGRect(x: -layoutFragmentFrame.minX, y: 0, width: width, height: layoutFragmentFrame.height) |
| |
820 | } |
| |
821 | |
| |
822 | override var renderingSurfaceBounds: CGRect { super.renderingSurfaceBounds.union(band) } |
| |
823 | |
| |
824 | override func draw(at point: CGPoint, in context: CGContext) { |
| |
825 | context.saveGState() |
| |
826 | context.setFillColor(Theme.blockBackground.cgColor) |
| |
827 | context.fill(band.offsetBy(dx: point.x, dy: point.y)) |
| |
828 | context.restoreGState() |
| |
829 | super.draw(at: point, in: context) |
| |
830 | } |
| |
831 | } |
| |
832 | |
| 807 | /// A heading line drawn with an ellipsis after it, like org's folded headings. |
833 | /// A heading line drawn with an ellipsis after it, like org's folded headings. |
| 808 | final class FoldedHeadingFragment: NSTextLayoutFragment, @unchecked Sendable { |
834 | final class FoldedHeadingFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 809 | override func draw(at point: CGPoint, in context: CGContext) { |
835 | override func draw(at point: CGPoint, in context: CGContext) { |