@@ -20,6 +20,7 @@ struct CodeTextView: View {
2020 let text : String
2121 let plainTextColor : Color
2222 let lineWrapping : CodeLineWrapping
23+ let layoutPreparation : CodeLayoutPreparation
2324 let highlightLanguage : CodeLanguage
2425 let highlightAppearance : CodeHighlightAppearance
2526 let highlightBatches : [ CodeHighlightBatch ]
@@ -34,6 +35,7 @@ struct CodeTextView: View {
3435 plainTextColor: plainTextColor,
3536 fontSize: fontSize,
3637 lineWrapping: lineWrapping,
38+ layoutPreparation: layoutPreparation,
3739 highlightLanguage: highlightLanguage,
3840 highlightAppearance: highlightAppearance,
3941 highlightBatches: highlightBatches
@@ -104,6 +106,7 @@ private struct CodeTextRenderRequest {
104106 let plainTextColor : Color
105107 let fontSize : CGFloat
106108 let lineWrapping : CodeLineWrapping
109+ let layoutPreparation : CodeLayoutPreparation
107110 let highlightLanguage : CodeLanguage
108111 let highlightAppearance : CodeHighlightAppearance
109112 let highlightBatches : [ CodeHighlightBatch ]
@@ -327,6 +330,31 @@ private struct CodeTextPreparedUpdate {
327330 let lineCount : Int
328331}
329332
333+ private struct CodeCompleteLayoutKey : Equatable {
334+ let generation : Int
335+ let containerWidth : CGFloat
336+ }
337+
338+ @MainActor
339+ enum CodeNativeLayoutPreparation {
340+ @discardableResult
341+ static func prepare(
342+ _ preparation: CodeLayoutPreparation ,
343+ layoutManager: NSLayoutManager ,
344+ textContainer: NSTextContainer
345+ ) -> Bool {
346+ let textLength = layoutManager. textStorage? . length ?? 0
347+ guard preparation. preparesCompleteLayout (
348+ forUTF16Length: textLength
349+ ) else { return false }
350+
351+ layoutManager. ensureLayout (
352+ forCharacterRange: NSRange ( location: 0 , length: textLength)
353+ )
354+ return true
355+ }
356+ }
357+
330358private extension CodeTextDocumentState {
331359 mutating func prepareUpdate(
332360 for request: CodeTextRenderRequest
@@ -454,6 +482,9 @@ private final class MacCodeTextContainer: NSView {
454482 private let gutterUpdates = CodeGutterUpdateCoordinator ( )
455483 private var lastAppliedHighlightSequence = - 1
456484 private var lineWrapping : CodeLineWrapping ?
485+ private var layoutPreparation = CodeLayoutPreparation . progressive
486+ private var layoutGeneration = 0
487+ private var completeLayoutKey : CodeCompleteLayoutKey ?
457488
458489 override var isFlipped : Bool { true }
459490
@@ -514,21 +545,26 @@ private final class MacCodeTextContainer: NSView {
514545 )
515546 gutterView. frame = frames. gutter
516547 scrollView. frame = frames. text
548+ prepareCompleteLayoutIfNeeded ( )
517549 scheduleGutterUpdate ( )
518550 }
519551
520552 func update( _ request: CodeTextRenderRequest ) {
553+ updateLayoutPreparation ( request. layoutPreparation)
521554 updateLineWrapping ( request. lineWrapping)
522555
523556 guard let update = documentState. prepareUpdate ( for: request) else {
524557 applyNewHighlightBatches ( from: request)
558+ prepareCompleteLayoutIfNeeded ( )
525559 scheduleGutterUpdate ( )
526560 return
527561 }
528562
529563 let visibleOrigin = scrollView. contentView. bounds. origin
530564 let selectedRanges = textView. selectedRanges
531565
566+ layoutGeneration &+= 1
567+ completeLayoutKey = nil
532568 textView. textStorage? . setAttributedString ( update. attributedText)
533569 lastAppliedHighlightSequence = request. highlightBatches. last? . sequence ?? - 1
534570 gutterView. updateMetrics (
@@ -541,6 +577,7 @@ private final class MacCodeTextContainer: NSView {
541577 }
542578
543579 layoutSubtreeIfNeeded ( )
580+ prepareCompleteLayoutIfNeeded ( )
544581 let destination = update. change. isNewDocument
545582 ? CGPoint . zero
546583 : visibleOrigin
@@ -563,6 +600,7 @@ private final class MacCodeTextContainer: NSView {
563600 to: textStorage
564601 )
565602 textStorage. endEditing ( )
603+ completeLayoutKey = nil
566604 lastAppliedHighlightSequence = batches. last? . sequence
567605 ?? lastAppliedHighlightSequence
568606 }
@@ -573,6 +611,7 @@ private final class MacCodeTextContainer: NSView {
573611 else { return }
574612
575613 self . lineWrapping = lineWrapping
614+ completeLayoutKey = nil
576615 textContainer. lineBreakMode = . byWordWrapping
577616
578617 switch lineWrapping {
@@ -593,6 +632,45 @@ private final class MacCodeTextContainer: NSView {
593632 textView. needsLayout = true
594633 }
595634
635+ private func updateLayoutPreparation(
636+ _ layoutPreparation: CodeLayoutPreparation
637+ ) {
638+ guard self . layoutPreparation != layoutPreparation else { return }
639+ self . layoutPreparation = layoutPreparation
640+ completeLayoutKey = nil
641+ }
642+
643+ private func prepareCompleteLayoutIfNeeded( ) {
644+ guard let layoutManager = textView. layoutManager,
645+ let textContainer = textView. textContainer
646+ else { return }
647+
648+ let textLength = layoutManager. textStorage? . length ?? 0
649+ guard layoutPreparation. preparesCompleteLayout (
650+ forUTF16Length: textLength
651+ ) else { return }
652+
653+ let containerWidth = lineWrapping == . word
654+ ? scrollView. contentSize. width
655+ : 0
656+ guard lineWrapping != . word || containerWidth > 0 else { return }
657+
658+ let key = CodeCompleteLayoutKey (
659+ generation: layoutGeneration,
660+ containerWidth: containerWidth
661+ )
662+ guard key != completeLayoutKey else { return }
663+ completeLayoutKey = key
664+
665+ CodeNativeLayoutPreparation . prepare (
666+ layoutPreparation,
667+ layoutManager: layoutManager,
668+ textContainer: textContainer
669+ )
670+ textView. layoutSubtreeIfNeeded ( )
671+ scrollView. reflectScrolledClipView ( scrollView. contentView)
672+ }
673+
596674 @objc private func clipViewBoundsDidChange( ) {
597675 scheduleGutterUpdate ( )
598676 }
@@ -689,6 +767,9 @@ private final class MobileCodeTextContainer: UIView, UITextViewDelegate {
689767 private let gutterUpdates = CodeGutterUpdateCoordinator ( )
690768 private var lastAppliedHighlightSequence = - 1
691769 private var lineWrapping : CodeLineWrapping ?
770+ private var layoutPreparation = CodeLayoutPreparation . progressive
771+ private var layoutGeneration = 0
772+ private var completeLayoutKey : CodeCompleteLayoutKey ?
692773
693774 override init ( frame: CGRect ) {
694775 super. init ( frame: frame)
@@ -718,21 +799,26 @@ private final class MobileCodeTextContainer: UIView, UITextViewDelegate {
718799 )
719800 gutterView. frame = frames. gutter
720801 textView. frame = frames. text
802+ prepareCompleteLayoutIfNeeded ( )
721803 scheduleGutterUpdate ( )
722804 }
723805
724806 func update( _ request: CodeTextRenderRequest ) {
807+ updateLayoutPreparation ( request. layoutPreparation)
725808 updateLineWrapping ( request. lineWrapping)
726809
727810 guard let update = documentState. prepareUpdate ( for: request) else {
728811 applyNewHighlightBatches ( from: request)
812+ prepareCompleteLayoutIfNeeded ( )
729813 scheduleGutterUpdate ( )
730814 return
731815 }
732816
733817 let contentOffset = textView. contentOffset
734818 let selectedRange = textView. selectedRange
735819
820+ layoutGeneration &+= 1
821+ completeLayoutKey = nil
736822 textView. textStorage. setAttributedString ( update. attributedText)
737823 lastAppliedHighlightSequence = request. highlightBatches. last? . sequence ?? - 1
738824 gutterView. updateMetrics (
@@ -745,6 +831,7 @@ private final class MobileCodeTextContainer: UIView, UITextViewDelegate {
745831 }
746832
747833 layoutIfNeeded ( )
834+ prepareCompleteLayoutIfNeeded ( )
748835 textView. setContentOffset (
749836 update. change. isNewDocument ? . zero : contentOffset,
750837 animated: false
@@ -765,6 +852,7 @@ private final class MobileCodeTextContainer: UIView, UITextViewDelegate {
765852 to: textView. textStorage
766853 )
767854 textView. textStorage. endEditing ( )
855+ completeLayoutKey = nil
768856 lastAppliedHighlightSequence = batches. last? . sequence
769857 ?? lastAppliedHighlightSequence
770858 }
@@ -773,6 +861,7 @@ private final class MobileCodeTextContainer: UIView, UITextViewDelegate {
773861 guard self . lineWrapping != lineWrapping else { return }
774862
775863 self . lineWrapping = lineWrapping
864+ completeLayoutKey = nil
776865 textView. textContainer. lineBreakMode = . byWordWrapping
777866 textView. textContainer. widthTracksTextView = lineWrapping == . word
778867
@@ -786,6 +875,42 @@ private final class MobileCodeTextContainer: UIView, UITextViewDelegate {
786875 textView. setNeedsLayout ( )
787876 }
788877
878+ private func updateLayoutPreparation(
879+ _ layoutPreparation: CodeLayoutPreparation
880+ ) {
881+ guard self . layoutPreparation != layoutPreparation else { return }
882+ self . layoutPreparation = layoutPreparation
883+ completeLayoutKey = nil
884+ }
885+
886+ private func prepareCompleteLayoutIfNeeded( ) {
887+ let textLength = textView. textStorage. length
888+ guard layoutPreparation. preparesCompleteLayout (
889+ forUTF16Length: textLength
890+ ) else { return }
891+
892+ let containerWidth = lineWrapping == . word
893+ ? textView. bounds. width
894+ : 0
895+ guard lineWrapping != . word || containerWidth > 0 else { return }
896+
897+ let key = CodeCompleteLayoutKey (
898+ generation: layoutGeneration,
899+ containerWidth: containerWidth
900+ )
901+ guard key != completeLayoutKey else { return }
902+ completeLayoutKey = key
903+
904+ textView. layoutIfNeeded ( )
905+ CodeNativeLayoutPreparation . prepare (
906+ layoutPreparation,
907+ layoutManager: textView. layoutManager,
908+ textContainer: textView. textContainer
909+ )
910+ textView. setNeedsLayout ( )
911+ textView. layoutIfNeeded ( )
912+ }
913+
789914 func scrollViewDidScroll( _ scrollView: UIScrollView ) {
790915 scheduleGutterUpdate ( )
791916 }
0 commit comments